[{"data":1,"prerenderedAt":270},["ShallowReactive",2],{"i-lucide:chevron-down":3,"i-lucide:arrow-up-right":8,"i-simple-icons:github":10,"i-simple-icons:x":12,"i-brand:linkedin":14,"i-simple-icons:medium":18,"i-lucide:mail":20,"i-lucide:sun":22,"project:\u002Fprojects\u002Fbulk-card-charging":24,"project-more:\u002Fprojects\u002Fbulk-card-charging":244,"i-lucide:briefcase-business":266,"i-lucide:calendar":268},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M7 7h10v10M7 17L17 7\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M14.234 10.162L22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299l-.929-1.329L3.076 1.56h3.182l5.965 8.532l.929 1.329l7.754 11.09h-3.182z\"\u002F>",{"left":4,"top":15,"width":16,"height":16,"rotate":4,"vFlip":6,"hFlip":6,"body":17},32,448,"\u003Cpath fill=\"currentColor\" d=\"M416 32L31.9 32C14.3 32 0 46.5 0 64.3L0 447.7C0 465.5 14.3 480 31.9 480L416 480c17.6 0 32-14.5 32-32.3l0-383.4C448 46.5 433.6 32 416 32zM135.4 416l-66.4 0 0-213.8 66.5 0 0 213.8-.1 0zM102.2 96a38.5 38.5 0 1 1 0 77 38.5 38.5 0 1 1 0-77zM384.3 416l-66.4 0 0-104c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9l0 105.8-66.4 0 0-213.8 63.7 0 0 29.2 .9 0c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9l0 117.2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":19},"\u003Cpath fill=\"currentColor\" d=\"M4.21 0A4.2 4.2 0 0 0 0 4.21v15.58A4.2 4.2 0 0 0 4.21 24h15.58A4.2 4.2 0 0 0 24 19.79v-1.093a5 5 0 0 1-.422.02c-2.577 0-4.027-2.146-4.09-4.832a8 8 0 0 1 .022-.708c.093-1.186.475-2.241 1.105-3.022a3.9 3.9 0 0 1 1.395-1.1c.468-.237 1.127-.367 1.664-.367h.023q.151 0 .303.01V4.211A4.2 4.2 0 0 0 19.79 0Zm.198 5.583h4.165l3.588 8.435l3.59-8.435h3.864v.146l-.019.004c-.705.16-1.063.397-1.063 1.254h-.003l.003 10.274c.06.676.424.885 1.063 1.03l.02.004v.145h-4.923v-.145l.019-.005c.639-.144.994-.353 1.054-1.03V7.267l-4.745 11.15h-.261L6.15 7.569v9.445c0 .857.358 1.094 1.063 1.253l.02.004v.147H4.405v-.147l.019-.004c.705-.16 1.065-.397 1.065-1.253V6.987c0-.857-.358-1.094-1.064-1.254l-.018-.004zm19.25 3.668c-1.086.023-1.733 1.323-1.813 3.124H24V9.298a1.4 1.4 0 0 0-.342-.047m-1.862 3.632c-.1 1.756.86 3.239 2.204 3.634v-3.634z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"m22 7l-8.991 5.727a2 2 0 0 1-2.009 0L2 7\"\u002F>\u003Crect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":23},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"page":25,"project":231},{"id":26,"title":27,"body":28,"description":222,"extension":223,"meta":224,"navigation":225,"path":226,"seo":227,"sitemap":228,"stem":229,"__hash__":230},"projectPages\u002Fprojects\u002Fbulk-card-charging.md","Bulk Card Charging",{"type":29,"value":30,"toc":207},"minimark",[31,36,40,51,55,58,81,85,93,113,117,134,138,145,149,156,159,164,171,174,178,185,188,192,199],[32,33,35],"h2",{"id":34},"overview","Overview",[37,38,39],"p",{},"Clever is Faris Petrol's payment card and app. The bulk charging system tops up many Clever cards from one merchant account: a spreadsheet becomes a batch that is checked, approved and sent card by card, with live progress and a clear outcome for each card. I designed and built it on my own in 2022, test-first, as a full-stack developer at Faris Petrol.",[41,42,48],"project-shot",{":single":43,"alt":44,"height":45,"src":46,"width":47,":bare":43},"true","Three screens of the dashboard, in Arabic. In front, a batch of cards with the approval dialog over it, asking for the password. Behind it on the left, the form for a new batch; on the right, a batch being sent, with a progress bar and a status for each card.",660,"\u002Fprojects\u002Fbulk-card-charging\u002Fbatch.webp",1760,[37,49,50],{},"The new batch form, the approval that asks for the password, and a batch being sent card by card.",[32,52,54],{"id":53},"the-problem","The problem",[37,56,57],{},"Charging a list of cards is easy to start and hard to get exactly right.",[59,60,61,69,75],"ul",{},[62,63,64,68],"li",{},[65,66,67],"strong",{},"Mistakes cost money."," A wrong amount or a missing row goes straight onto someone's card.",[62,70,71,74],{},[65,72,73],{},"Not every answer is clear."," With no reply from the platform, nobody knows if a card was charged.",[62,76,77,80],{},[65,78,79],{},"Long batches need watching."," Operators must see progress, stop a batch, and keep a record.",[32,82,84],{"id":83},"the-solution","The solution",[86,87,90],"sketch-figure",{"alt":88,"name":89},"Without it: a list of cards and amounts goes to the card platform unchecked. Nobody checks the totals or approves the list, with no answer nobody knows if a card was charged, and sending it again may charge it twice. With it: upload with totals, approve with a password, highlighted, then one job per card. Every card ends processed (charged), failed (safe to send again) or stuck (a person checks), with live progress and a record.","bulk-card-charging\u002Fbefore-after",[37,91,92],{},"A list becomes a batch that is checked and approved first, and every card ends with a status you can trust.",[59,94,95,101,107],{},[62,96,97,100],{},[65,98,99],{},"Checked and approved first."," The file must match the typed totals, and an approver confirms with a password.",[62,102,103,106],{},[65,104,105],{},"Failed and stuck, kept apart."," An error is safe to send again; no reply holds the card for a person.",[62,108,109,112],{},[65,110,111],{},"One job per card, live."," Each card is its own queued job, and the page updates as each one is answered.",[32,114,116],{"id":115},"what-i-did","What I did",[59,118,119,122,125,128,131],{},[62,120,121],{},"Designed and built the system on my own: the back end, the dashboard, the queue and the live progress.",[62,123,124],{},"Wrote the specification: the states of a batch and a card, and what each action may do in each.",[62,126,127],{},"Built it test-first, with a test for every action and every way the platform can fail.",[62,129,130],{},"Connected it to the Clever API; a merchant account signs in once, and its password is never stored.",[62,132,133],{},"Set up roles, registered devices, and a password check to approve, send, cancel or close a batch.",[32,135,137],{"id":136},"what-it-offers","What it offers",[86,139,142],{"alt":140,"name":141},"Everything the system does, in four columns. Set up: merchants with their Clever accounts, users and roles, registered devices. Run a batch: upload a spreadsheet whose totals must match, approval with its own permission and the password, cancel, and close by hand with a note. Every card: one queued job per card, live progress and, highlighted, three outcomes: processed, failed or stuck. Follow up: settle a stuck card, resend failed cards, an error log and an Excel export.","bulk-card-charging\u002Ffeatures",[37,143,144],{},"From setting up merchants to following up on every card, built around the three outcomes.",[32,146,148],{"id":147},"system-design","System design",[86,150,153],{"alt":151,"name":152},"Architecture: an operator and an approver use the dashboard of one web app, open to registered devices only, which asks for the password again on key steps and holds merchants, batches and their cards, the error log, the export, and users and roles. A queue, highlighted, runs one locked job per card against the Clever card platform, which answers with an invoice or an error. Live updates push every change back to the page, and a database keeps it all.","bulk-card-charging\u002Farchitecture",[37,154,155],{},"One web app, a queue that sends each card on its own, and live updates back to the page.",[37,157,158],{},"One web app, open only to registered devices, checks each spreadsheet and keeps the batches. An approved batch goes to a queue where every card is a job that calls the Clever API, and each answer is pushed back to the page.",[160,161,163],"h3",{"id":162},"the-life-of-a-batch","The life of a batch",[86,165,168],{"alt":166,"name":167},"A batch is uploaded with totals that must match, approved with a permission and a password, and sent as one job per card. It ends processed, highlighted, when every card is charged, or partly processed when closed by hand with a note. It can be cancelled at any point: cancelled if nothing was sent, partly cancelled otherwise. A stuck card must be settled first, and results export to Excel.","bulk-card-charging\u002Fbatch-lifecycle",[37,169,170],{},"A batch is sent only after it is approved, and it can be stopped at any point before the end.",[37,172,173],{},"Uploading and approving are separate permissions. A merchant account has one open batch at a time, and closing a batch by hand needs a note.",[160,175,177],{"id":176},"failed-or-stuck","Failed or stuck",[86,179,182],{"alt":180,"name":181},"Each card’s job sends a top-up to the card platform, locked so it can’t go out twice. An invoice means processed, highlighted: the card was charged. An error means failed: nothing was charged and it is safe to send again. No reply means stuck: the outcome is unknown, so a person checks the platform, marks it processed or failed and confirms with a password. A batch can’t close or be cancelled while a card is stuck.","bulk-card-charging\u002Ffailed-or-stuck",[37,183,184],{},"An error and a silence lead to different places, because only an error proves nothing was charged.",[37,186,187],{},"The system never sends a stuck card again by itself. A person checks it on the platform and settles it, and that rule is what keeps a card from being charged twice.",[32,189,191],{"id":190},"in-the-product","In the product",[41,193,196],{":single":43,"alt":194,"height":45,"src":195,"width":47,":bare":43},"Three screens of the dashboard, in Arabic. In front, a batch waiting for approval, with approve and cancel buttons and its cards, each not yet sent. Behind it on the left, the list of merchant accounts; on the right, the list of batches with a date filter.","\u002Fprojects\u002Fbulk-card-charging\u002Fscreens.webp",[37,197,198],{},"Merchant accounts, a batch’s cards waiting for approval, and the list of batches.",[41,200,204],{":single":43,"alt":201,"height":202,"src":203,"width":47},"One batch being sent, in Arabic: the merchant, the number of cards and the total at the top, a progress bar, then the cards. One is in progress, one failed with a connection error and has a resend button, and the rest are not sent yet.",820,"\u002Fprojects\u002Fbulk-card-charging\u002Fbatch-running.webp",[37,205,206],{},"A batch mid-run: one card in progress, one failed with a connection error and a resend button, the rest waiting.",{"title":208,"searchDepth":209,"depth":209,"links":210},"",2,[211,212,213,214,215,216,221],{"id":34,"depth":209,"text":35},{"id":53,"depth":209,"text":54},{"id":83,"depth":209,"text":84},{"id":115,"depth":209,"text":116},{"id":136,"depth":209,"text":137},{"id":147,"depth":209,"text":148,"children":217},[218,220],{"id":162,"depth":219,"text":163},3,{"id":176,"depth":219,"text":177},{"id":190,"depth":209,"text":191},"How I built bulk top-ups for Clever cards at Faris Petrol: a spreadsheet checked against typed totals, approval with a password, one queued job per card, and a person for any card without a clear answer.","md",{},true,"\u002Fprojects\u002Fbulk-card-charging",{"description":222},{"loc":226},"projects\u002Fbulk-card-charging","6OA6mr-GwFiHSdt76tP2dFLJC7gi18WSaqLOFUJ7Mh0",{"id":232,"title":233,"context":234,"description":235,"extension":236,"highlight":237,"links":238,"mark":239,"meta":241,"order":209,"stem":229,"year":242,"__hash__":243},"projects\u002Fprojects\u002Fbulk-card-charging.yml","Bulk card charging for Clever","Faris Petrol · Oct – Dec 2022","Charges many Clever cards at once from a spreadsheet: checked, approved, then sent card by card with a clear result for each. Built test-first.","yml",null,[],{"logo":240},"clever",{},2022,"K7uhiSB6HaLlYlsSSrqB6DcX-365WCOZUZneu_n8twQ",[245,252,259],{"path":246,"title":247,"description":248,"mark":249,"year":251},"\u002Fprojects\u002Frunarks","Runarks · Governed access to production databases","My own product: every query against a production database is reviewed, approved, run once and logged, and nobody sees a password.",{"logo":250},"runarks",2026,{"path":253,"title":254,"description":255,"mark":256,"year":258},"\u002Fprojects\u002Fsanad","Sanad · Real-estate investment platform","The API and admin dashboard I built for a Saudi real-estate investment platform: verified investors, a wallet, investments and returns paid on a schedule.",{"logo":257},"sanad",2024,{"path":260,"title":261,"description":262,"mark":263,"year":265},"\u002Fprojects\u002Fwinch","WINCH · Saudi logistics platform","The back end of a Saudi logistics platform for businesses and carriers: more than 50 domains, and the integration engine I built for government services, payments and AI.",{"logo":264},"winch",2023,{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":267},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M12 12h.01M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2m14 7a18.15 18.15 0 0 1-20 0\"\u002F>\u003Crect width=\"20\" height=\"14\" x=\"2\" y=\"6\" rx=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":269},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M8 2v3m8-3v3\"\u002F>\u003Crect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"\u002F>\u003Cpath d=\"M3 9h18\"\u002F>\u003C\u002Fg>",1791781461127]