المنتجاتمباشر
Keforo App
مكتب الطلبات لفرق التجارة الإلكترونية في المنطقة: طلبات ومخزون وعملاء وورقة Google Sheets تبقى متزامنة.
- الحالة
- مباشر
- السنة
- 2024
- الدور
- مؤسّس
كل الطلبات في لوحة واحدة بدورة حياة صارمة، ومخزون يتحرّك في المعاملة نفسها مع الحالة، وموصّل Google Sheets لا يحتاج مراجعة تطبيق من Google، ومساعد ذكاء اصطناعي لا يفعل إلا ما يُسمح به لمن يسأله. بالعربية والفرنسية والإنجليزية.
ما هو
- الطلب آلة حالات: نقطة نهاية لكل انتقال، ونطاق صلاحية خاص للحركات المالية الحساسة، وآثار المخزون في معاملة MongoDB نفسها
- مزامنة Google Sheets بثلاث طرق للاتصال، وربط أعمدة يكشف الانحراف، وسياسة تعارض لكل ورقة، ومهمة واحدة لكل تشغيل في طابور
- مساعد ذكاء اصطناعي من منسّق وخمسة متخصصين، كل أداة فيه تتحقّق من صلاحيات المتصل قبل أن تعمل
- Bun وHono خلف واجهة Next.js، 386 مسارًا، 68 نطاق صلاحية في 18 وحدة، 585 اختبارًا للخلفية
How it works
An order as a state machine, one Google Sheets sync replayed, an assistant that can only do what you may, billing by bank transfer, and why the stack is what it is.
هذه الوثائق غير مترجمة بعد؛ تُعرض بالإنجليزية.
#What it runs
Only what the code runs today. Every line below is live in the product, in Arabic, French and English, with a real right-to-left layout.
- Orders. One board for every order with filters that live in the URL, a strict lifecycle, line items that can be cancelled or marked out of stock one by one, partial returns that restock, and an activity log with 18 kinds of entry.
- Stock. Warehouses and inventory in two buckets, on hand and committed. Every change writes a stock movement that points back at the order, import or return that caused it.
- Customers, countries and cities. Who ordered, and the delivery zones a shop serves. An order carries its payment method, cash on delivery, card, wire or other, and its payment status.
- Google Sheets. A sheet linked to the app is read on a schedule or on demand, mapped column by column, and upserted without touching rows that did not change.
- Imports. CSV and Excel files through the same processors, with progress checkpoints so a browser refresh does not lose the run.
- Team. Members, roles and a scope matrix: 68 scopes across 18 modules, edited per person. Team chat rooms over the same WebSocket.
- Assistant. A chat that reads orders, products and KPIs and can take a few actions, with a confirmation step and a permission check on every tool.
- Billing. Plans, paid by bank transfer with an uploaded proof and an admin review. Renewals, grace periods and suspensions run on cron.
#How a request moves through the system
The assistant has its own diagram further down; this is the path an order or a sheet takes.
يُرسم المخطط
#An order is a state machine
Every status change in the system goes through one service. It checks the move against a table, applies what the move does to stock, writes the new status and appends to the activity log, all inside one MongoDB transaction. If any part fails, none of it happened.
يُرسم المخطط
Eight statuses, two of them terminal. A shipped order cannot be cancelled from the app, because the parcel is already with the courier; it is delivered and then refunded.
#Try it
Pick a token, then press an endpoint. The route refuses a missing scope before the handler runs, the service refuses a move that is not in the table, and a legal move changes the status, moves stock and writes the log.
#The rules the code enforces
- One endpoint per transition. There is no
PATCH status. Place, confirm, pack, ship, deliver, cancel and refund are seven routes, each with its own validation, its own scope and its own line in the activity log. - Money-sensitive moves have their own scope. Confirm, pack, ship and deliver share
orders:transition. Cancel isorders:canceland refund isorders:refund, so an operations agent can run the day without being able to undo a sale. - Stock and status move together. Cancelling releases the committed units, delivering removes them from on hand and committed, refunding puts them back on hand. The stock write comes first, inside the same transaction, so a failed stock update aborts the transition.
- Every stock change is a row. A stock movement records the bucket, the before and after, the reason and the order it belongs to. Inventory can always be explained.
- Atomic guards, not checks. The update that sells a unit requires on hand and committed to still cover the quantity; if a concurrent write got there first, the update matches nothing and the move fails with 409.
- Line items have their own life. A line can be cancelled, marked out of stock or restored while the order is still in the shop; each releases or re-reserves exactly its units. If every line is cancelled, the order follows.
- Returns restock. From shipped or delivered, a partial return of one or more lines credits stock back and writes its own movement, under the refund scope.
- Re-applying a status does nothing. The same request twice is a no-op, not a second stock operation.
#One sheet, one run
Most shops in the region run on a Google Sheet before they run on anything else. The connector treats the sheet as the source and the app as the mirror: each run reads the sheet and pushes every row through the same import processor a CSV upload uses, and nothing is written back to the sheet.
Commandes · sheet “Ventes 2026”
orders · daily 06:00 · sheet wins
“6,214 rows, 13 mapped columns, sheet wins, last synced yesterday at 06:00”
- 1queue.add google-sheet-syncone job per sheet, from the cron or the Sync now button—
- 2SyncRun.status = runningwritten before the adapter starts, so the browser shows it without polling—
- 3drive.files.get versionwatermark 118 → 121: the sheet changed since the last run—
- 4mapping.checkDrift13 headers still where the mapping expects them—
- 5sheets.values.get ×25,000 rows per page, two pages, under the per-org limit of 60 calls a minute—
- 6orders.import ×6214212 new orders, 5,987 existing ones replaced from the sheet, 15 left alone because they are already packed or later—
- 7LinkedSheet.lastDriveVersion = 121the next run compares against this and skips an unchanged sheet after one call—
- 8SyncRun.counts + errorSample0 failed; the error sample is capped at 1,000 rows—
- 9ws.emit syncRun:finishedthe organisation’s room hears it at once—
#Three ways to connect
Google reviews every app that asks for Drive access, and the review takes time. So the connector has three doors, and two of them need no review at all.
يُرسم المخطط
#The rules inside the connector
- Mapping is proposed, then confirmed. Headers are matched to fields with a confidence of exact, normalized, fuzzy or unmapped. You fix what the matcher got wrong, then a dry run shows what would be written before anything is.
- A moved column pauses the sheet. Each run checks that every mapped header is still there. If one is gone, the sheet becomes
mapping_drift, the person who connected it gets one email, and the wizard proposes a re-map. Nothing is guessed at run time. - One conflict policy per sheet. Database wins leaves an existing order alone; sheet wins and latest-updated replace it from the row. An order that is packed or later is never touched by a sheet, whatever the policy. Chosen once, applied to every row.
- Rows are matched by reference, not by guess. An order keeps the sheet’s reference in its source, and a partial unique index on organisation, platform and reference makes a re-sync, a stalled job re-run or a duplicate worker harmless: the second insert is refused and the row counts as skipped.
- Cadence is a cron or an interval. Every 4 hours to yearly, anchored at 06:00 Casablanca time for the daily and longer ones; every 15 minutes and hourly exist in the code, hidden until a paid tier.
- One job at a time. The sync worker runs one job per process with a two-minute lock. Two runs upserting the same collection tripped MongoDB write conflicts in a cascade, so concurrency is one, on purpose.
- No automatic retry. A failed run is a run someone has to look at: a gone sheet, a lost connection, a moved column. Retrying would fail the same way and hide the cause. Three failures in a row mark the sheet
failingand switch it back to manual sync until someone re-arms it. - Sixty calls a minute per organisation. A fixed window in Redis mirrors Google’s own per-user ceiling. A job that hits it is re-queued with the wait time the limiter reports.
- Cancel is cooperative. A flag is checked between pages. A cancelled run writes the counts it reached and stops.
- A run always ends. Queued, running, then success, partial success, failed, cancelled or skipped because nothing changed. The worker marks a crash as failed rather than leave a run running.
#The assistant only does what you may
The chat in the sidebar is an orchestrator that owns no domain knowledge and one tool: delegate. It hands a sub-task to a specialist, in parallel when a question spans domains, and writes the answer from what comes back.
يُرسم المخطط
- Permission is checked on every tool call. The first line of every tool is
requireScope. The scopes come from the caller’s token; a model saying “the user is an admin” changes nothing. Wildcards work the way they do in the API, and the system bypass never crosses an organisation boundary. - Specialists you cannot use do not exist. A specialist whose tools need scopes the caller lacks is left out of the orchestrator’s prompt, so the model cannot even try.
- Writes wait for a person. Cancelling an order or changing a price creates a pending action that the person confirms in the chat. The scope is checked again at confirmation.
- Models are an allow-list. Four tiers from premium to economy, each entry with its vendor, context window and cost per million tokens. Provider errors are classified and fall back along a chain instead of failing the turn.
- Every turn leaves a trace. Which specialist ran, which tools, how long, how many tokens. Admins read traces, a turn can be replayed, and a long conversation is compacted by a prompt rather than truncated.
- Three languages in the prompt too. Locale overlays for Arabic, French and English, so the assistant answers in the language the shop works in.
#Billing by bank transfer
Most businesses here do not pay by card. A subscription is a bank transfer with a photo of the receipt, reviewed by an admin, then advanced month by month by a cron.
يُرسم المخطط
- A request is reviewed, not trusted. The proof is uploaded, an admin approves or declines with a reason, and the organisation’s plan changes only on approval.
- The cron does the calendar. One lifecycle job advances the billing period each month, warns when the paid months run out, gives three days of grace with a reminder email that carries the bank details, and suspends after that by dropping the plan to free.
- Stale requests expire. A request nobody reviewed in seven days becomes
expired, so the queue an admin sees is always current. - Card payment is written, not switched on. A Stripe service with idempotency keys per operation and a retry helper exists in the code; the card tab in the app is disabled. Bank transfer is the live path.
#Under the hood
- Tokens. The access token lives in memory; the refresh token never reaches the browser. It sits in an encrypted, HTTP-only cookie that only a server action reads. Protected routes check a signed role cookie before rendering.
- Tenancy. Every document carries its organisation. A middleware loads and tenant-guards the order before a handler sees it, and background jobs do the same lookup with their own guard.
- Routes as data. Each route declares its method, path, scopes and schema; one declaration feeds the router, the permission check and the request validation. There are 386 of them.
- Realtime. A WebSocket server native to Bun, on the same port as HTTP, with rooms for everyone, one person, one organisation and one chat room. Ten thousand connections, fifty per address, ten per person, 64 KB per message, a circuit breaker that resets after 30 seconds, Redis pub/sub across instances.
- Queues. Four BullMQ workers: sheet sync, sheet cron, imports, billing lifecycle. Sync and imports write progress as they go, so the browser follows a run without polling.
- Errors in three languages. Every module carries its own messages in Arabic, French and English; a 409 says why in the language of the shop.
- Tests. 585 backend tests in 52 files against MongoDB and Redis, including scenario tests for the assistant; 65 tests on the front.
#Why this stack
- Bun and Hono. TypeScript end to end, no build step, one runtime for the API, the workers and the scripts. Hono stays out of the way and lets routes be declared as data.
- MongoDB with transactions. An order, its lines, its payment summary and its metadata are one document, and the stock update that goes with a status change is in the same transaction. Fifty models, each carrying its organisation.
- Redis and BullMQ. A sync run reads thousands of rows and cannot live inside an HTTP request. BullMQ gives a durable job, a lock only one worker holds, a concurrency cap and a repeat schedule that is either a cron or an interval. Redis also holds the per-organisation rate limit and the WebSocket fan-out.
- A WebSocket, not polling. The worker reports itself when a run ends, the chat streams, notifications arrive. One socket, rooms per audience.
- Next.js and TanStack. Filters, sort and page live in the URL so a view is a link. Tables are virtualised because an order list is long, and queries are cached and invalidated by the events the socket delivers.
- One gateway for models. Every assistant call goes through OpenRouter, so the model behind a specialist is a configuration row with a cost, not a dependency.
- Google Sheets first. It is where the orders already are. Reading a sheet well, with mapping, drift detection and hashing, is worth more to a shop here than a store connector it does not use yet.
#Not there yet
- Shopify, WooCommerce, YouCan and Salla are listed as coming soon. A WooCommerce OAuth controller is scaffolded; nothing syncs from a store today.
- The sheet is read, never written. Changes made in the app do not flow back to the spreadsheet.
- No per-row change detection yet. Under sheet wins, every editable order in the sheet is rewritten on each run; only the whole-sheet version check saves an unchanged sheet from a full pass.
- Card payment through Stripe is coded but switched off; bank transfer is the only live way to pay.
- No shipping-carrier integration. Delivery zones are countries and cities; the courier is outside the app.
- No exports, and no mobile app in this code base.
هل أفادك هذا؟
النقاش
التالي
imgkit