ملاحظات الإصدارات

كل بناء مسلَّم، بتاريخه وما تغيّر فيه.

تُولَّد من سجل تغييرات المنتج مع كل بناء لهذا الموقع. السطور نفسها تُشحن مع برامج التثبيت وتظهر في البوابة. النص بالإنجليزية، لغة سجل التغييرات.

0.18.9أحدث إصدار
  • Requested by the Product Owner: the licence portal added to the website as in CertiHANA, and everything prepared to host the full site (spec W2, defaults recorded there).
  • Licence portal (website/portal, PHP 8.1 / MySQL, no dependencies, served at /portal/): one vendor login; Issue tab signs CertiPOS licences (tenant slug, register and user seats, modules, optional node lock, validity; ES256 compact JWS exactly as Es256LicenceVerifier reads it), Clients with licence history, reissue (supersede) and recording of tokens issued elsewhere, Requests from the website's demo form (notification + acknowledgement, status, notes, convert to client), Files (chunked uploads, SHA-256, revocable share links), Settings (identity, SMTP with password / Microsoft 365 app / relay, reminders); daily expiry reminders from cron. Brand 1.0 UI with the site's own fonts. Harnesses on SQLite: licensing_test.php, requests_test.php, files_test.php.
  • Vendor key: tools/licensing/gen-vendor-key.php created the CERTICONS signing key (license-private.pem, git-ignored, to be backed up offline); its public key is shipped in appsettings.json (CertiPos:License:PublicKeyPem), so every installed server verifies portal-issued licences. PortalLicenceTests proves a portal-signed token in the product's verifier (skipped where php is absent) and that the shipped key is a valid P-256 key.
  • Website: the demo form posts to /contact.php of its own origin by default (the portal stores and mails it; PUBLIC_LEADS_ENDPOINT still redirects it to the server's /web/leads); footer link to the portal (nofollow), robots.txt disallows /portal/ and /contact.php; public/.htaccess with HTTPS, canonical host, headers, caching and the 404 page; npm run package writes installer/output/CertiPOS-website-<version>.zip for the document root.
  • Portal of the product (System → Licence): a licence request block (organisation slug, seats in use, version) with Copy and E-mail request.
  • Docs: docs/Website_Hosting_cPanel.md (installation, SMTP options, cron, daily workflow, backup, troubleshooting), website/README.md, website/portal/README.md, tools/licensing/README.md.
0.18.8
  • Pilot findings (Product Owner, 2026-10-13, Operations page): a cash sale whose invoice posted and whose payment failed was reposted after the configuration fix; the repost voided the job and invoiced the customer again (three invoices for one ticket, two left open in SAP).
  • Posting operations (spec M3 §8a): Repost adopts the documents of the job it supersedes and resumes after them (never a second invoice); Void is refused for a job that created documents in SAP (new E-PST-106, the portal disables the button) unless a reversal cancelled them; Reverse works on a failed or voided job whose documents exist (the reverse job names its target, cancels the payments and the invoice, marks the ticket reversed); an invoice already in SAP for the ticket (comment marker, same customer, not cancelled) is adopted before a new one is created, and an invoice recorded on the job that was cancelled in SAP is dropped. E-PST-102 reworded. PilotFixes0188Tests prove it live on SBODEMOGB.
  • No register change; both installers are rebuilt at 0.18.8 for the version.
0.18.7
  • Pilot finding (Product Owner, 2026-10-13): the register answered "The server cannot be reached" when enrolling with a typed code against https://…:5443, while the portal was open in a browser on the same machine. A typed code carries no certificate fingerprint (only the QR does), so the self-signed server certificate failed the platform validation and the handshake failure was reported as a network outage.
  • Register (spec M17 §2a): when an enrolment without a pinned fingerprint fails in the TLS handshake, the register reads the certificate the server presents without sending anything, shows its SHA-256 fingerprint in groups of eight (LTR island) and asks the operator to compare it with the TLS fingerprint printed under the QR code in the portal; Trust and enrol pins it exactly as a scanned QR would, Cancel leaves it. A handshake failure where no certificate can be read is the new E-NET-004, with the next step. The enrolment form scrolls on a short screen. Tests against a real TLS server with the test certificate: enrolment_certificate_test.dart (probe, controller paths, screen in EN and AR).
  • No server change; both installers are rebuilt at 0.18.7 for the version.
0.18.6
  • Pilot findings on the server installer wizard (Product Owner, 2026-10-13): (1) a first install tried to stop services that were not installed and showed the error; (2) after an uninstall through the wizard the next install could not create the services.
  • Installer (installer/CertiPOS-Server.nsi): a service is asked to stop only when it is installed and not already stopped (the log says "not installed; nothing to stop" or "already stopped" instead). The uninstaller stops, deletes and then waits until Windows has really removed each service (a deleted service stays "marked for deletion" while its process runs or a Services console holds it; after 60 s the uninstaller names the cause); it skips the backup task and the firewall rules that do not exist and writes %TEMP%\CertiPOS-Server-uninstall.log. The installer creates the CertiPOS service when missing and reconfigures it when present; on error 1072 (marked for deletion by an earlier uninstall) it waits and creates it anew; over a kept data directory (uninstall without /DROPDATA) it registers the PostgreSQL service again on the existing database, which the old script never did (it only started a service that was no longer there and failed with E-INS-002). New catalogue code E-INS-003: a Windows service could not be created, configured or started, with the next step.
  • Drill (installer/drill-installers.ps1): the uninstall steps wait for the uninstaller process itself and give the services no grace period; a new pair of steps runs the wizard flow of the finding: uninstall keeping the data, reinstall at once, both services running, the tenant of before signs in with its register. docs/Installer_Guide.md §6 and docs/contracts/errors.md updated.
0.18.5
  • Requested by the Product Owner on 0.18.4: a full, SEO-ready website for CertiPOS with the most creative design and complete content, as an enterprise solution, not SaaS. Approved on the design canvas "CertiPOS Website Design" ("proceed as drawn, domain certipos.certicons.com, form to the server outbox").
  • Website (spec W1, folder website/): a static Astro 5 site at certipos.certicons.com, thirty-eight pages in English, Arabic (right to left) and French: home, product (overview, register, portal, SAP Business One, offline, restaurants, hardware, security, platform), six industries, how it runs (deployment, hosting, requirements, operations), licensing (plans, services, questions and answers), resources (library, deployment guide, SAP integration, security overview, release notes generated from the changelog, three articles), company, partners, contact, demo, privacy, legal. Brand 1.0 throughout (indigo, IBM Plex self-hosted, the receipt motif, real screenshots only), one layout with canonical, hreflang for the three languages, Open Graph with a rendered preview image, JSON-LD (Organization, WebSite, SoftwareApplication, BreadcrumbList, FAQPage, Article), sitemap index and robots; no external script, font or image. Build gates: every slug in every language, titles and descriptions within limits. CI job website. Prices, sizing, addresses and partner terms are bracketed placeholders for the Product Owner to fill.
  • Demo requests through the server (POST /web/leads, anonymous, rate-limited like a sign-in, CORS for the website origin only): the form becomes one e-mail in the outbox of the tenant named by CertiPos:Website:TenantSlug, sent to CertiPos:Website:LeadsTo, plus an audit row; a honeypot hit is accepted and dropped; E-WEB-001 for missing name, company or a bad e-mail address, E-WEB-002 when the server is not configured for it. Contract and error catalogue updated.
0.18.4
  • Requested by the Product Owner on 0.18.3: the tender screen's ticket column showed four lines without their quantity and the amounts table squeezed its cells (the LBP row ran into the next column, the ticket row had no thousands grouping).
  • Ticket column of the tender screen laid out properly: the column is wider (460 dp, 500 dp from 1360 dp); every line is listed with its quantity, name and amount, with the unit price, discount, promotion, modifiers, serials, weight and note on a second line, voided lines struck through; the customer (name and code, or walk-in), the table with its covers and the ticket note stand above the list; a Totals block (subtotal, discount, tax per code, rounding, total) and the Amounts in every active currency table (currency column, then plain grouped numbers under Total / Paid / Still due at one size, nothing scaled) follow, with the rates behind the rows for operators who may see them (a stale rate names its day); the tendered list appears only once something was tendered; the still-due card groups thousands. On a lane the lines scroll in their own area and the totals, amounts, change and tenders stay in view below them (they scroll only past half the column).
0.18.3
  • Found by the Product Owner on 0.18.2: a repeated scan of the same item added a new line although New line per scan was off on the tenant and the register.
  • New line per scan honoured: the register never read the setting (it existed in the catalogue since M2 but every entry appended a line). Off or unset: entering a plain item again (scan, typed code, item field, quick button) raises the quantity of its open line, the line is selected and the stock check runs on the new quantity. On: every entry is its own line, as before. A line with a serial, batch, weight, price barcode or B1 base, a restaurant line (seat, course, modifiers, sent to the kitchen), a voided line and a line changed by hand (price override, discount, note, promotion) is never joined: the next entry starts a new line.
0.18.2
  • Found by the Product Owner on the register of 0.18.1: the end group of the top bar (pills, operator, clock) was shrunk to a third of the bar and sat in the middle.
  • Top bar laid out properly: the start group (back, mark, register, ticket, customer) and the end group (pills, operator, clock, menu) sit at their natural size on their edges with the free space between them. Nothing is scaled: as the bar narrows it drops detail in tiers (the sync pill, the float on the shift pill, the operator id, then the name), and pills are 28 dp with an icon each.
0.18.1
  • Found by the Product Owner on the register of 0.18.0: the top bar's groups sat in the middle, the tender screen was not the one drawn (the tender types and the cash panel floated mid-screen, the amounts table wrapped), and the selected line's actions wrapped badly.
  • Top bar: the start group (mark, register, ticket, customer) hugs the start edge and the end group (pills, operator, clock, actions) hugs the end edge; a narrow bar scales them down instead of pushing them around. The mark carries the brand letter.
  • Tender: the three columns fill the height; the amounts column is wider and its cells never wrap (currency / total / paid / due); the tender types are the buttons drawn (56 dp, tinted icon square, label, the amount as an LTR island, cash green, card blue, stored value amber); the cash panel gained the keypad and a "Next note" shortcut (the smallest note that covers what is due in the selected currency); "Tendered" sits inside the column.
  • Line actions: −, quantity, +, Price, Discount and Void line on one row; labels that named their key in words no longer repeat it next to the keycap.
  • Notices move to the centre under the status bar: at the end edge they covered the cash panel's top buttons.
  • Tests: tender_screen_m7_test reads the label and the amount as two texts; full register suite.
0.18.0
  • Approved by the Product Owner on the design canvas "CertiPOS Register Design" (spec docs/specs/M19_Register_Design_Config_Spec.md): the register's design 1.0, the configuration catalogue and the SAP account pickers.
  • Register, every lane screen: a status bar at the top (store and register, the ticket, the customer chip, then pills for Online / Offline with the rows waiting to send, the last sync, held tickets, the shift with its float, training mode, the operator, the clock) and an action bar at the foot with every function on a labelled key (Hold F4, Recall F5, Returns F6, Cash in / out F10, Void line F8, Void ticket F9) and one filled primary action with the amount on it (Pay … F12). Every colour means the same thing everywhere: green good, amber attention, red stop, blue information; the one filled button is the primary action.
  • Lane: the cart with a header row, line number, name, quantity × price, badges for serial / batch / weighed (blue), promotion and discount (green), low stock (amber), voided (red); the selected line is tinted with a marker on the start edge and carries its own actions (−, quantity, +, Price, Discount, Void line) next to the typed wildcards; the totals card ends with the total in the brand colour and the equivalent in every other active currency; the item field results show the price and the cached stock. Phone (under 720 dp): the same parts one below the other with the actions at the foot.
  • Tender: three columns: the amounts (ticket summary, a table of total / paid / due in every active currency, tenders taken, still due), the tender types, and the cash panel (currency tabs, notes with their pictures, exact, amount, the change card). Esc goes back, F12 completes.
  • Home menu: a "today" strip (sales and tickets of the shift, cash in the drawer per currency, held tickets, rows to send) and the tiles grouped Sales, Cash and shift, Inventory, Restaurant, Device, each with its tint and a hint (the open ticket, the version); the Checkout tile is outlined while a ticket is open.
  • Notices: found by the Product Owner: the black notice at the foot of the register covered the action bar. Every notice is now a tinted card under the status bar at the end edge that never covers a button, closes by itself (4 s, errors 8 s) or with its ×, and a new one replaces the old.
  • Configuration profiles (portal): every setting has a name, a description, a unit or example, and the right editor (switch, number, select, account picker, secret, JSON with an error line); settings sit in named groups with a search box and an "only set here" filter; an empty field shows the value in force one level up; the "in force on a register" view names the setting and the level it comes from. GET /admin/config-keys answers this catalogue (it answered the bare key → type map before).
  • SAP account pickers: every account field in the portal (configuration, Currencies → Accounts) is a type-to-filter list fed from the company's chart of accounts, showing code, name and currency, filtered by what the field is for: cash accounts (SAP's cash-account flag) for drawer and safe, balance-sheet accounts for card / cheque / transfer / clearing, liability accounts for deposits, stored value, points and tips payable, revenue and expense accounts for breakage and points expense; a currency narrows to accounts in that currency or in all currencies; "every account" escapes the filter. The chart is read with the SAP sync as the entity ChartOfAccounts (table gl_accounts, migration M19_GlAccounts, never pulled to registers) and GET /admin/companies/{id}/accounts?role=… serves it. After the upgrade, run Sync now once under SAP and sync.
  • Tests: m19_register_design_test.dart (notice placement and closing, status bar, lane header / line actions / action bar, phone layout, tender columns), the RTL passes and every existing lane and tender test (keys kept); ConfigCatalogTests (every known key explained, account roles); M2FlowTests live (chart synced from SBODEMOGB, cash role finds 170400, revenue role excludes it, catalogue served); RLS table count; pilot-upgrade migration list; the configuration page and the account picker clicked in a browser on the dev server; full server and register suites.
0.17.7
  • Asked for by the Product Owner: the item field of the register must find an item by item code, name, barcode, serial number and so on, as the CertiScan item field does, as a smart field.
  • The item field matches more: one typed text is looked for in the item code, the name, the foreign name, every barcode and (from three characters) every serial number SAP knows, case-insensitively and anywhere in the value. Exact matches lead, then prefixes, then contained matches. Every row shows the name, the code and, when a barcode or a serial matched, which one ("Barcode 4006…", "Serial A6-000001"). The lane and the catalogue use the same field.
  • A serial names its item: picking a serial row, or pressing Enter on an exactly typed or scanned serial, adds the line with that serial number, so a serial-managed item needs no second entry. Whether the serial is still in stock stays SAP's decision at posting (the Service Layer does not expose the stock status of a serial).
  • Serial numbers reach the register: the server syncs the serial numbers of SAP (SerialNumberDetails) as the entity SerialNumbers: every poll reads the rows added since the last one, the daily full poll re-reads everything; the registers receive them with pull kind 27 (table serial_numbers with row-level security, migration M18c_SerialNumbers; register schema 15). After the upgrade, run Sync now once (or wait for the poll), and the registers re-pull their master data by themselves.
  • Enter picks what the text names: the field resolves the typed text first (wildcard, scan, exact code, barcode, serial) and offers its single candidate only when the text names nothing; it never acts on the candidate list of an earlier text (a cleared field had kept it, and Enter could add the wrong item after a quick key).
  • Contract: MasterDataPull.serialNumbers (kind 27); sync entity SerialNumbers. Spec M18 §6f.
  • Tests: master_data_test.dart (ranking, barcode / serial / manufacturer number / foreign name, inactive serial hidden, three-character rule), checkout_screen_test.dart (rows for a typed serial and a typed barcode, Enter on a serial adds the line with it), M2FlowTests live (serials synced from SBODEMOGB, the delta reads nothing new, the pull carries them), RLS table count, pilot-upgrade migration list; full server and register suites.
0.17.6
  • Found by the Product Owner on the pilot server: a posting failed with E-PST-099 Unexpected failure while posting: InvalidOperationException: The node must be of type 'JsonObject'. SAP had refused a document, and the server crashed while reading SAP's error answer: it read error.message.value (the /b1s/v1 shape) and the Service Layer had answered error.message as a plain string (the /b1s/v2 shape, also used by some framework errors). SAP's reason was lost and the job retried as transient with the same message.
  • Service Layer answers are read defensively: both error shapes (message object or string, code number or string), a bare value instead of an object, and both odata.nextLink / @odata.nextLink page links; nothing in the client throws on what SAP answers (ServiceLayerAnswer, Domain). A refused document now fails with SAP's own error code and text, mapped to the catalogue as before (E-CUR-021, E-PST-010, …), and the raw answer is on the job in Operations.
  • Unexpected posting failures are logged: an E-PST-099 writes the full exception with stack trace to the server log (logs\certipos-<date>.log) and names the log in the message; the portal still never shows a stack trace.
  • After the upgrade, retry the failed job in Operations: the message becomes SAP's actual reason.
  • Tests: ServiceLayerAnswerTests (both shapes, odd answers, page links); full server suite.
0.17.5
  • Found by the Product Owner on the register of 0.17.4: banknote buttons with a picture spread over the whole row and the pictures were cropped. A banknote button now has a fixed footprint per size (small 96 × 48, medium 136 × 64, large 184 × 88 dp, about 2.1 : 1 like a note), so a row of notes lines up, and the picture is stretched to the button, never cropped. Quick-layout tiles fill their cell the same way.
  • Recommended image sizes, stated in the portal next to the banknote rows and on the image page: small 192 × 96, medium 272 × 128, large 368 × 176 pixels (twice the footprint for sharp screens); tiles 120 × 120 per cell.
  • Tests: image_buttons_test.dart (footprint and stretch); full register and server suites.
0.17.4
  • Asked for by the Product Owner: the image library (System → Customer display) was upload and on/off only.
  • Edit an image: a dialog per row changes the name, the purpose (slide, banner, logo, banknote, tile), the scope (every store or one store), the order and the state, and can replace the file under the same id: every currency or layout that uses the image shows the new picture after the registers' next pull (the SHA-256 changes, the registers fetch it again).
  • Remove an image: a confirmation per row; the row stays as a switched-off tombstone with a new version so every register drops its copy with the next pull, and its bytes are dropped on the server (migration M18b_DisplayMediaRemoval, column deleted_at_utc). Removed images leave the list and their file answers 404.
  • In use: an image that is a banknote of a currency or a tile of a quick layout can neither be removed nor given another purpose (E-DSP-003 names where it is used; the dialogs show it and disable Remove). GET /admin/display-media/{id}/uses lists the uses.
  • Contract: DisplayMediaChange takes purpose, companyId, storeId, contentType and contentBase64; DELETE /admin/display-media/{id}; GET /admin/display-media/{id}/uses.
  • Tests: M15DisplayTests (edit, re-purpose refused while in use, removal refused while in use, replacement reaches the register with the new bytes, removal reaches the register as a switched-off row, the file answers 404), pilot-upgrade migration list; the dialogs checked in a browser on the dev server.
0.17.3
  • Asked for by the Product Owner during the pilot (three requests, taken as a patch on the M18 branch with the defaults named in spec M18 §6d).
  • Banknotes per currency with an image and a size (Portal → Sales → Currencies → edit): every note is a row with its value, an optional image behind the button and the size of the button (small / medium / large); values can still be typed as a list. The images are uploaded under System → Customer display with the new purpose banknote (PNG or JPEG, 2 MB), reach the registers with the master data pull like the display images and work offline. The register's tender screen draws the note as the button background with the value on a dark band (CPImageButton), sizes 48 / 56 / 80 dp; without the file yet the button stands plain. Stored as numbers when plain (an older register still reads them), objects {value, image, size} otherwise; E-CUR-005 names a value, image or size that is not valid.
  • Quick-layout buttons with an image: the layout designer offers the images of purpose tile for every button and shows them on the grid; the register draws the tile behind the button's label and now also honours the button's colour. E-QLY-004 names an image that is not a switched-on tile image.
  • Portal session end: when the session has ended, the portal signs out and returns to the sign-in page with "Your session has ended. Sign in again to continue where you were." (and comes back to the page afterwards), whether a click met the ended session or the portal's own check found it gone (every minute, GET /api/v1/auth/portal/session with a probe header that does not extend the session). Until now the page stayed open and every button failed silently. The portal also keeps the cookie the server renews (sliding expiration), so a long working day no longer ends in a dead page. Session length from the configuration: CertiPos:Portal:SessionMinutes (default 720, sliding).
  • Tests: domain PilotEnhancementTests (banknote parsing and storage, layout image, purposes), register image_buttons_test.dart (banknotes as numbers and objects, image button rendering and sizes, a quick tile with its image, plain buttons without the file); the session probe, the uploads with the new purposes, the banknote dialog, the layout designer and the session end checked in a browser on the dev server.
0.17.2
  • Found by the Product Owner on the pilot: USD and LBP enabled, rated and mapped, yet the register still offered GBP only. The shift of the register had been open since 2026-09-25, a ticket carries the business date of its shift, and the register looked for a rate on or before that date; the rates exist from 2026-10-02 on. The register now takes the rate of the day the cash changes hands (the later of the business date and today), so a shift left open over several days works with the rates of today.
  • Register, asked for by the Product Owner: the tender screen shows the total and the amount due in every active currency (the ticket currency large, each other currency on its own line, thousands grouped: 28,800,000 LBP); every tender, whatever its currency, lowers the due of all of them (10 USD received lowers the GBP, USD and LBP due by its equivalent); at zero every line turns green. A foreign tender in the list shows what was handed over in its own currency with its decimals and, for operators with tender.rate.view, the rate as "1 USD = 0.7519 GBP" (it showed the raw SAP quotation). The "exact" button and the change proposals group thousands too.
  • Register: after an upgrade of the app the master data is pulled again from the start, once (MasterDataApplier.restartAfterUpgrade). The server is upgraded first, so its new data (enabled currencies, rates) reached registers of the older version, which stored what they knew and moved their resume version past it; after their own upgrade they never asked for those rows again.
  • Register: the cash in / cash out / safe drop dialog offers the currency selector when a shift is already open (the list of currencies had not been loaded yet).
  • Tests: m18_tender_screen_test.dart (total and due in three currencies before and after a 10 USD tender and at zero, a shift opened a week ago with rates from today, the re-pull after an upgrade, grouping); full register and server suites.
0.17.1
  • Found by the Product Owner on the pilot of 0.17.0: USD and LBP were enabled and mapped in the portal, yet the tender screen of the register offered GBP only. A register offers a currency only when it holds a rate for it, and the server read the SAP rate of a newly enabled currency only at the next pass of the rate worker (up to half an hour later); until then the register hid the currency without a word.
  • Server: enabling a currency reads its SAP rates of today and tomorrow at once, so the registers offer it with their next synchronisation (one minute). A company connected before 0.17.0 gets its currency facts (local and system currency, rate quotation, accuracy) from SAP the first time a rate is read, instead of waiting for the next connection test (SapConnectionService.EnsureCurrencyFactsAsync; the quotation is never guessed: E-CUR-003 when SAP does not answer it). The local and system currency are now read from the SAP administration data (the company information of Service Layer does not carry them), so they are stored and E-CUR-001 can fire.
  • Posting, found by the full live suite (0.17.0 had run the M18 and portal tests only): a cash sale in the ticket currency alone asked for a clearing account (E-PST-010, after the invoice and the payment were posted) whenever SAP's invoice total differed from the ticket's by a rounding cent, for example with a discount folded into the unit price. CashAllocation now exchanges nothing when all the cash is in the ticket currency: the payment carries what the invoice needs, as before M18.
  • Server: the currency endpoints answer 404 E-CMP-006 for a company that does not exist or belongs to another tenant (they answered 500); the RLS table count includes currency_rates.
  • Register: an enabled currency the register holds no rate for is named under Cash ("No exchange rate for LBP on this register. Sync the register, or set the rate in the portal under Currencies."), EN / AR / FR, instead of being hidden.
  • Portal, Sales → Currencies: a warning above the tabs lists what keeps an enabled currency from working: no exchange rate (the registers do not offer it), no cash account (its payments cannot post), no clearing account (change in another currency cannot post).
  • Left in SBODEMOGB by the two failing runs of the M10 live test before the fix: invoices 2863 and 2866 (45.61 each, paid by incoming payments 2065 and 2067), not reversed.
  • Tests: the full server suite and the full register suite ran for this build; M18DomainTests (cash in the ticket currency only never exchanges), M18PostingTests (live on SBODEMOGB: the rate is there right after the switch and in the register's pull; a company without currency facts gets them when a currency is enabled; the USD + GBP sale with LBP change still posts and reverses), m18_tender_screen_test.dart (segments per rated currency, the hint for a currency without a rate, Arabic pass).
0.17.0
  • M18 multi-currency cash (spec docs/specs/M18_Multicurrency_Spec.md, asked for by the Product Owner during the manual test): a receipt may be paid in cash of several currencies (50 USD + the LBP for the rest), the change is given in whichever currency the customer wants, and SAP receives one incoming payment per currency on the same invoice plus the exchange of the drawer as journals.
  • Portal: Sales → Currencies (right admin.currencies): the SAP currencies with a POS-enabled switch, decimals, rounding unit (1,000 for LBP notes), banknotes and order; Rates: today's effective rate per currency as SAP quotes it (direct / indirect), fetch from SAP, a manual rate with validity and reason pushed into SAP (ratePushToSap, cap manualRateMaxDeviationPercent, right admin.rates, beyond the cap admin.rates.override), the history of 30 days; Accounts: the cash, over/short and rounding account per currency and the all-currencies clearing account, each checked against its currency in SAP (E-CUR-022).
  • Server: currency_rates table (RLS) and CurrencyRateWorker (SAP rates of today and tomorrow every half hour, manual rates pushed when SAP disagrees), the company's local / system currency and rate quotation read at every connection test (E-CUR-001 when the base currency differs), pull kind 26 currencyRates and the POS attributes on kind 0, config keys ticketCurrency, currencyAccounts, changeCurrency, manualRateMaxDeviationPercent, ratePushToSap, cashBanknotes per currency; intake re-checks every foreign amount (E-CUR-010) and the enabled currencies (E-CUR-011); cash-sale posts the cash per currency (CashAllocation: foreign currencies take the invoice first, the ticket currency the residual, never more cash than applied), one incoming payment per currency (DocCurrency, DocRate, SumApplied / AppliedFC) and the exchange of cash between currencies as journals through the clearing account with explicit local amounts, all resumable (extraDocuments) and reversed by the reverse process; over/short at shift close per currency; SL errors "exchange rate not updated" → E-CUR-021 (retry), currency refusals → E-CUR-022; rights tender.change.foreign, tender.rate.view, admin.currencies, admin.rates, admin.rates.override; role templates updated; error catalogue E-CUR-*; contracts (TicketTender.rateQuote / rateSource, TicketChange, TicketTotals.rounding, the currencies endpoints).
  • Register: rates cached offline (drift schema 14, currency_rates, the effective rate resolved like the server: manual in force, else the day's SAP rate, else the latest earlier one flagged stale), the cash panel with a segment per currency, notes per currency, "exact" in the selected currency, the due shown in every other currency, the rate of the day for operators with tender.rate.view; the change card (give the change in any enabled currency on its notes, or split it; recorded as events, withdrawn when a tender changes; required before completion only when more than one cash currency is enabled); float per currency, drawer movements with a currency, the X / Z report subtracts the change and the refunds in their own currency; receipt with the foreign amount, the rate and the local value and the change per currency; customer display with the tender currencies; CurrencyMath mirrored from the server with shared vectors (docs/contracts/vectors/currency_math.json).
  • Proven live on SBODEMOGB (probe M18ProbeTests in three rounds, then M18PostingTests: USD + GBP cash with LBP change → invoice, two payments, two journals, all reversed); the demo's local currency is GBP with indirect quotation (1 GBP = 1.33 USD), which the probe showed the earlier foreign-cash conversion of the register got backwards. Tests: server domain 224, API 27 + portal navigation, integration M18 and portal green; register 361 incl. m18_register_test.dart; portal checked in a browser; exit drill prepare-m18 4 + pos/tool/m18_drill_test.dart green with every document posted and reversed on SBODEMOGB (spec §6a).
  • Spec deviations (§7): rates kept as SAP quotes them (not normalised); rounding remainders journalled, not UnderOverpaymentdifference (SAP ignores it); exchange journals one currency each through the clearing account; the change card only with more than one cash currency; rateMissing alert as a log warning; ticketCurrency other than local built into the posting but not yet exercised live; payments not yet in the POS vs B1 reconciliation.
0.16.4
  • Register tender dialogs redesigned (asked for by the Product Owner during the manual test of 0.16.3: "the CSS of these windows is not good"). One TenderDialog scaffold (lib/widgets/tender_dialog.dart) carries every tender dialog: a title with the tender's icon in a tinted square, the fields spaced evenly at 420 dp, the amount in large tabular figures with the ticket currency as a suffix, references with a leading icon and without the "0/4" counter, hints and limits in a note block, Cancel and one wide primary button. Applied to card (standalone and manual), cheque, bank transfer, custom tender, gift card, store credit, points, offline card, refund card, bar tab open / top-up and tip adjustment; every widget key stays, so the tests and the drills are unchanged.
  • Theme: a dialog theme generated from the brand (tools/dev/apply_brand.py: card surface, 12 px radius, soft shadow, title in the headline style, even paddings) so every dialog of the register, not only the tender ones, wears the same frame.
  • Tests: register 358 green (the M15 offline-card and M17 RTL-pass checks read the fields by their keys), analyzer clean, brand check clean.
  • Analysis delivered for the multi-currency cash feature (currencies in the portal, G/L account per payment method and currency, rates from SAP with a manual rate, one receipt in two cash currencies, change in either currency, two payments per invoice): docs/specs/M18_Multicurrency_Spec.md on branch m18-multicurrency, draft awaiting the decisions of §9.
0.16.3
  • Fixes and enhancements asked for by the Product Owner during the manual test of 0.16.2.
  • Register: the sign-in and the unlock no longer freeze the screen: the PIN and the badge are verified in another isolate (PinHasher.verifyAsync) and the button shows a progress ring meanwhile; the date and time of the device tick on the sign-in, menu and checkout bars and on the lock screen (ClockText, Latin numerals, left to right); the version, build number and build date stand on the sign-in, lock, enrolment and menu screens and on the device page (VersionLabel; tools/bump-version.py writes appBuild and appBuildDate); the lock screen scrolls on a small screen.
  • Portal: the CERTICONS byline in the drawer foot is sized and links to https://certicons.com; the version and build date of the server stand under it and in the foot of the sign-in, second-step, two-step and first-run pages (PortalInfo, the values of /api/v1/meta); the badge of the alert bell sits on the bell.
  • Installers: an end-user licence agreement (installer/assets/LICENSE.txt) shown by both installers; the server executable carries the brand icon (ApplicationIcon), both installers register DisplayIcon, the publisher link and the build date in Programs and Features; the register installer adds a firewall rule for certipos.exe (customer display and store hub on the LAN) and removes it at uninstall; the server installer's firewall rules of 0.16.2 stay.
  • Found by the register installer of 0.16.2 and corrected there: an existing installation is recognised (upgrade page, the running app is closed, enrolment and data kept).
  • Installers, found on the pilot: the scripts are read as UTF-8 (the dashes of the role page showed as mojibake); the server installer stops PostgreSQL before it replaces the files under pgsql and waits for both services to stop, PostgreSQL is started again before the migrations run; the finish text of the register installer fits its line; the shortcuts of the register name the icon of the program, the old shortcuts are removed and the shell is told, so a desktop that cached the icon of an earlier build shows the brand mark.
  • Tests: register 358 (pilot_fixes_test.dart: clock format and LTR islands, isolate verdict equals the synchronous one; the login and lock tests verify on the calling isolate under the fake clock), server API 46 and portal integration 11 green; portal checked in a browser.
0.16.2
  • Brand applied (docs/BRAND.md 1.0 of 2026-10-01, made with Claude Design from docs/BRAND_BRIEF.md): mark, wordmark, byline with the CERTICONS logo, app icons for every platform, installer bitmaps, customer display idle screen, empty-state glyphs, IBM Plex Sans + IBM Plex Sans Arabic + IBM Plex Mono (SIL OFL 1.1, bundled), colour tokens for a light and a dark theme with their contrast ratios, the type scales of the register and the portal. branding/ is the source of truth; tools/dev/apply_brand.py writes it into the apps (--check in CI), tools/dev/apply_brand_icons.ps1 the iOS icon set.
  • Register: certipos_theme.dart generated from the tokens (CPColors, CPColorsDark, CPFonts, CPType, CPRadius; floor-plan and kitchen-display tones), fonts and brand assets bundled, Arabic set in Plex Arabic at × 1.08 with no letter spacing, the Windows / Android (adaptive) / iOS icons; the brand mark printed at the top of every receipt as a compiled-in GS v 0 raster (receiptLogo wordmark / mark / none, receiptWidthDots 384 / 576); the customer display shows the mark by default and wears the dark theme; the kitchen display uses the kds tokens. Found by the pass: the weight step and the handover screen overflowed at 360 dp with the new type scale (they scroll).
  • Portal: palette, :root tokens and @font-face generated; the mark in the top bar and on the sign-in pages, the CERTICONS byline in the drawer, favicons; floor-plan and live-table colours from the tokens.
  • Server: the PDF export embeds the bundled IBM Plex Sans Arabic before it looks for a font of the machine. Installers: icon, header and wizard images. Manual: accent colours.
  • Tests: register 355, server API 46 / domain 215 / portal and PDF integration 11 green; brand check clean; portal checked in a browser (fonts, mark, byline, Arabic in Plex Arabic).
0.16.1
  • Portal navigation and design (enhancement asked for by the Product Owner during the manual test of 0.16.0; no change to the API, the data or the register).
  • Navigation: the 47 pages are organised in nine sections (Overview, Sales, Restaurant, Customers, Catalogue, Reports, People and access, Integration, System) from one map (Layout/PortalNav.cs); a section shows only the pages the signed-in user has a right to and is not shown when it has none; the section of the current page is open, the others fold; a finder in the menu narrows it to the pages whose name or keywords match; a "Go to a page" box in the top bar jumps to any page by name. "Operations" is named "Posting queue".
  • Design: one stylesheet of the portal (wwwroot/css/portal.css, served by the server itself, within the Content Security Policy) and a refined theme (neutral surfaces with borders and hairline shadows, one brand colour for actions, 268 px navigation with uppercase section labels and an active marker, white top bar with the brand mark, table headers in small capitals, dates and numbers that do not wrap, chips, tabs, dialogs and inputs aligned); a shared page header on every page with the section above the title and one line on what the page is for; references to specification sections removed from what the customer reads; the account menu (two-step sign-in, sign out) replaces the loose icons; the sign-in, second-step, two-step settings and first-run pages carry the same look through classes instead of inline styles.
  • Proven in a browser on the development server: sign-in, dashboard, 47 pages with the header, the finder, the account menu, the right-to-left preview; API tests 46 and the portal, security review and two-step sign-in integration tests 16 green.
0.16.0
  • M17 (branch m17-hardening-terminals, spec docs/specs/M17_Phase2_Hardening_Spec.md, product plan §5b M17, ASVS L2 gaps V2.8, V9.1.3, V14.4.3, V7.2, decision D4) — Phase 2 hardening: two-step sign-in of the portal, HTTPS only, a Content Security Policy with a nonce, the audit of reads, the sign-off gate of terminal adapters, CERTICONS' security review as tests, the performance budgets and the RTL pass of Phase 2, the upgrade in place from the pilot. Decisions D-M17-3 to D-M17-7 taken as proposed; D-M17-1 (adapters of vendors) and D-M17-2 (external penetration test) stay open with the Product Owner.
  • Two-step sign-in (TOTP, RFC 6238): enrolment with QR code or key on the page Two-step sign-in, a code that signs in once, ten recovery codes kept as hashes, lockout like the password, policy portalMfa off / optional (default) / admins / required with an enrol-only session (E-MFA-004), reset of another user by an administrator and never the own (E-MFA-006), the journal records enrolment, verification, failure, recovery and reset. Users page shows who signs in with two steps.
  • HTTPS only: installation switch CertiPos:RequireTls (before any credential; the health probe from the machine itself is answered) and tenant setting requireTls (sessions, sign-in, enrolment, device tokens, the device door, receipt and pay pages); plain HTTP answers E-NET-003 with the https address; enrolment codes for https addresses only; the register refuses a code that pins a certificate against an http address; Maintenance → Security and check show the state. New backend installations write the switch; an upgrade changes nothing. The portal reaches its own API over HTTPS with the certificate of the server (by its fingerprint) and shares no cookies between circuits; a store hub reaches a backend with its own certificate by the pinned fingerprint (CertiPos:Hub:UpstreamCertificateSha256, installer page, /UPSTREAMFP); the certificate loads without administrator rights.
  • Content Security Policy: script-src 'self' and the nonce of the request (alphabet without + and /), default-src 'self', connect-src 'self' and the WebSocket of the host, img-src 'self' data: blob:, font-src 'self' data:, style-src 'self' 'unsafe-inline' (the component library writes style elements without a nonce; accepted remainder, checklist V14.4.3); the portal loads no font from another server; guest pages (receipt, board, pay) carry the nonce on their script.
  • Audit of reads: the register journals reads of customers and their documents, loyalty members, gift card balances, tickets of others recalled or printed again, delivery addresses (outcome read, kind and key, never the content); settings auditReads off / sensitive (default) / all, auditReadSearchThreshold (20), auditReadsPerShiftAlert (100) with the alert readVolume; report Exceptions → Data reads; the Permission journal shows the outcome.
  • Terminal sign-off gate: terminal_signoffs per company and adapter (record, revoke with a reason), portal Terminals → Sign-offs, register configuration terminalAdaptersAllowed; a register whose configuration names an adapter without a sign-off runs the standalone terminal and says so on Peripherals (E-TRM-009); standalone and simulator exempt, trial licences exempt; Installer guide section 10 holds the checklist. No adapter of a vendor (D-M17-1).
  • Security review as tests: six tests generated from the endpoint map of the running server (anonymous surface against a signed list, rights of every route, tenant isolation, register isolation, malformed input on every route, secrets in no answer). Found and corrected: support bundles of registers readable across tenants; configuration secrets (smtpPassword, b1EventSecret, terminal.password) answered to the portal, written to the audit log and the bundle and sent to registers (now masked, and old audit rows masked by the migration); 500 on malformed requests (now E-REQ-004); an open redirect of the sign-in (/\host); the sweep of test clusters.
  • Performance budgets of Phase 2 (server: intake 17 ms, pull of 5,000 rows 78 ms, public API page 10 ms, feed page 21 ms, webhook staging 595 ms, styles 81 ms, deliveries 11 ms; register: promotion engine 5.6 ms, barcode parser 35 ms for 10,000 labels, grid 12 ms, delivery list 27 ms, scan to line 5 ms on 50,000 items) and the RTL pass of 19 register screens at 360 dp and 1280 dp plus 25 portal pages in the RTL preview. Found and corrected: the grid of a style read its members one by one (218 ms); checkout and tender had no phone layout and overflowed at 360 dp, the tender screen of a table check also at 1280 × 800; gift card sheet, signature pad, weight step and kitchen display overflowed on a phone; Arabic was letter-spaced by the type scale (the Arabic theme spaces nothing); the handover named a tender by its code; E-NET-003 without its code.
  • Upgrade in place: UpgradeFromPilotTests on a database made by the sources of 0.8.1 (Fixtures/pilot-0.8.1.sql) and m17_upgrade_test.dart on a register database of drift schema 7 made by the register sources of 0.8.1 (test/fixtures/register-0.8.1.sqlite; open shift, outbox, held and open tickets kept, chain goes on); drill-installers.ps1 -UpgradeFrom for the installer (elevated hand step); Installer guide "From 0.8.1 to 0.16.0".
  • Also corrected: a shift over midnight counted zero worked minutes of today (M6 time clock); a test of it that failed between midnight and three o'clock.
  • Persistence: migration M17_Phase2Hardening (user_mfa, user_mfa_recovery, terminal_signoffs with RLS; audit rows masked). Errors: E-MFA-001 to 006, E-NET-003, E-TRM-009, E-TRM-010, E-REQ-004. Contracts: M17 block of openapi.yaml generated from the code; outcome read and mfa on the session in the schemas. Rights: unchanged in number, portalMfa policy binds to the security rights of the catalogue.
  • Docs: manual page Security (EN, AR), error appendix (308 codes), Installer guide (HTTPS only, from 0.8.1, terminal sign-off), pilot runbook, ASVS checklist after M17 (open: inline styles, external test, the defaults of the switches, reads in the portal, vendor adapters).
  • Tests: server 389 (domain 215, architecture 4, API 46, integration 124 with every live fact) incl. the portal on real HTTPS sockets; register 354. Installer of the server built (0.16.0). Evidence, deviations and findings in the spec §8 Evidence and §8a.
  • Open hand steps (each needs the person at the machine or the account of CERTICONS): the installer drills of 0.16.0 (drill-clean.ps1, then -UpgradeFrom with the installer of 0.8.1) and the register installer from an elevated shell; one click in the portal over https:// on the installed server; the upgrade of the pilot after the runbook; adapters of the terminal vendors when devices and documents are on site; an external penetration test. Still open from M16: MSIX install drill on a clean machine, the signed iOS build, the sign-off of a physical scale; from M15: a WhatsApp message through the Twilio sandbox, the Power BI Desktop refresh.
0.15.0
  • M16 (branch m16-verticals-mobility, spec docs/specs/M16_Verticals_Mobility_Spec.md, catalogue 1.2, 1.19, 9.6, 21.1, 21.2, 17.2, 17.3, 18.1) — verticals and mobility: selling by weight, weight and price labels, the colour / size matrix, delivery packages handed over at the door on a mobile register, MSIX and iOS packaging. Decisions D-M16-1 to D-M16-8 taken as proposed.
  • Scales: register contract ScaleAdapter with the adapters simulator, line (continuous or on-demand text output, pattern configurable) and mt8217 (Mettler Toledo 8217), over a serial port (Windows) or TCP; ScaleService is the only caller; status on the peripherals screen and in the heartbeat. Weight step on the checkout (live weight, fixed tares, tare from the scale, zero, typed weight); the line carries weight (gross, tare, net, unit, source scale / barcode / manual) and its quantity is the net weight in the unit the item is sold in (kg, g, lb, oz); a weighed quantity is never typed (E-SCL-005). Rights sales.weight.manual, sales.weight.tare, device.scale.zero. The adapters are proven against simulators; a physical scale needs its sign-off. Legal metrology is outside CertiPOS (scaleLegalNotice prints the notice of the store).
  • Weight and price barcodes: pure parser mirrored in C# and Dart with one vector file; masks (weightBarcodeMasks: prefix, item, weight or price, check digit; lookup by barcode or by the tail of the item code) and GS1 element strings (01, 10, 11, 13, 15, 17, 21, 30, 37, 310n, 320n, 392n, 393n; bracket, raw and symbology-identifier forms); a price label comes out at its printed price, the rounding difference within weightBarcodePriceTolerance as the line discount; batch, serial and expiry from the label, an expired item needs sales.expired.override; intake check on the server with the alert weightBarcodeException; portal barcode test.
  • Colour / size matrix: item_styles / item_style_members kept on the portal (page Styles) or imported from the item fields U_CP_Style, U_CP_Colour, U_CP_Size (on demand, and with styleItemFields at every sync of the items; the portal wins); pull kind 24; grid on the register with price and cached stock, keyboard-operable, RTL; right sales.matrix.open; quick layout kind style; ticket lines carry styleCode; reports by style, style and colour, style and size; feed entity Styles; size runs in inventory documents (sizeRuns).
  • Deliveries: delivery_packages, delivery_package_lines, append-only delivery_events; portal page Deliveries (open sales orders live from the Service Layer, package per order or part of it, assign, take back, plan again, close, cancel, history with names); pull kind 25 (a register sees its own packages, store pickups with deliveryStorePickup); register screens Deliveries and Handover (out for delivery, quantities per line with a reason for a part, balance with the tenders of the register, signature, failed with a reason), all offline with the outbox kind delivery; recovery after a kill between signature and completion. Rights delivery.view, delivery.handover, delivery.fail, portal admin.deliveries; sales.order.create and sales.delivery.now bound. Alert deliveryException; webhooks delivery.assigned, delivery.completed, delivery.failed; public API GET /api/public/v1/deliveries (scope deliveries.read); feed entity Deliveries.
  • Posting: ticket kind order posts a sales order, a down payment invoice for the prepayment and its incoming payment (process sales-order); the handover posts an A/R invoice based on the order lines that draws the down payment (deliveryPosting invoice) or a delivery with payments on account (deliveryNote); "deliver now, invoice later" posts a delivery (process delivery-note); the reversal of a handover opens the package again, the reversal of an order ticket cancels it. Roll-up and reconciliation know the new documents.
  • Packaging: installer/build-msix.ps1 (MakeAppx, SignTool, self-signed test certificate or the production certificate by path; identity CERTICONS.CertiPOSRegister) and installer/drill-msix.ps1; iOS build configuration (bundle id, deployment target 15.0, usage texts, export options) and docs/Packaging_iOS.md; managed app configuration on Android and iOS (serverUrl, enrolmentCode, certificateSha256): a register enrols without typing.
  • Found by the exit drill and the live tests and corrected: a scale reading under way while the configuration changed threw on reporting its status; styleItemFields was accepted and never read; a size imported later was appended instead of placed in its natural order; the history of a package showed the id of the user; the live count test of M12 dated its count by UTC between local and UTC midnight.
  • Persistence: migration M16_VerticalsMobility with RLS on five tables, ticket_lines.weight_json and style_code; register drift schema 13. Errors: 23 new catalogue codes (E-SCL-001 to 005, E-BCD-001 to 005, E-STY-001 to 005, E-DLV-001 to 008). Contracts: M16 block of openapi.yaml and openapi-public.yaml generated from the code (tools/dev/m15_contract_gen.py), asyncapi.yaml with the delivery events.
  • Docs: manual pages "Weighed items, colours and sizes, deliveries" (register) and "Scales, labels, styles and deliveries" (portal) in EN and AR, error appendix regenerated, Installer guide §7a (MSIX), docs/Packaging_iOS.md.
  • Tests: server 341 (architecture 4, domain 196, API 46, integration 95 with every live fact) incl. five live M16 postings on SBODEMOGB, each reversed; register 295. Exit drill green with generation 27 (prepare-m16, pos/tool/m16_drill_test.dart, m16-report, post-all, m16-cleanup); portal checked in a browser; evidence, deviations and findings in the spec §7 and §7a.
  • Open hand steps (each needs the person at the machine or the account of CERTICONS): MSIX from a fresh register build and its install drill on a clean machine (the exit criterion "MSIX installs on a clean machine" is open until then), the signed iOS build on a Mac, the sign-off of a physical scale. Still open from M15: a WhatsApp message through the Twilio sandbox, the Power BI Desktop refresh.
0.14.0
  • M15 (branch m15-platform-channels, spec docs/specs/M15_Platform_Channels_Spec.md, catalogue 15.16, 15.14, 4.7, 15.10, 15.21, 16.6, 4.8, 11.4, 16.8) — platform and channels: CertiPOS opens to other systems (public API, webhooks, data feed) and gains the channels around the sale (SMS and WhatsApp, customer display content and capture, remote actions, scheduler, offline card policy, report designer basics). Decisions D-M15-1 to D-M15-9 taken as proposed.
  • Technical users and public API: api_clients with scopes, optional company / stores, limits per minute and per UTC day (429 with Retry-After), key cpk_<ULID>.<secret> shown once (SHA-256 of the secret stored), optional client certificate pinned by thumbprint; Bearer or Basic; /api/public/v1 with cursor paging (tickets, shifts, items, prices, stock, customers, loyalty, gift cards, reports) and idempotent writes (customer creation posts a business partner through the existing customer-create process); contract docs/contracts/openapi-public.yaml. Portal page API clients.
  • Webhooks: events written in the transaction of the change (transactional outbox), signed CertiPOS-Signature: v1=HMAC-SHA256(secret, timestamp + "." + body), retries after 1 / 5 / 30 minutes and 2 / 6 / 24 hours, then dead; 50 dead deliveries disable the subscription (alert webhookFailing); private targets refused (SSRF guard at validation and at connect time); subscriptions of a technical user see only its data. Portal page Webhooks.
  • Notifications: plug-in contract IMessageProvider with plugins/CertiPos.Messaging.Simulator and plugins/CertiPos.Messaging.Twilio (SMS and WhatsApp; status callbacks verified by the vendor signature), provider settings protected in message_providers, message outbox with retry, templates per channel / kind / language with whitelisted variables and LTR islands in Arabic, append-only consents; alerts on message channels (alertChannels), waitlist and reservation messages. Portal page Notifications.
  • Receipts: link by SMS or WhatsApp from the register (rights receipt.send.sms, receipt.send.whatsapp; outbox kind receipt, works offline); receipt page PIN (receiptPinMode off / lastFour / code, five wrong attempts lock 15 minutes, E-RCP-004), the mode follows the store of the ticket.
  • Remote actions: queue register_actions (forceSync, lock, pushConfig, restart, shutdownAt, message, wipe) delivered by the heartbeat and acknowledged with the result; central actions per store / company / tenant; wipe of a retired or blocked register with a second administrator (E-ACT-003, E-ACT-004) through a device-signed door, journaled. Portal page Remote actions.
  • Scheduler: scheduled_jobs / job_runs, schedules daily / weekly / monthly / every N minutes in the timezone of the store, missed runs collapse, one run at a time; kinds report.email (CSV / XLSX / PDF attachments), alert.query, dayClose, rollUp, repost.failed, cleanup (retention keys, incl. support bundles), giftcard.expire, loyalty.expire, archive.export, campaign.send, dashboard.refresh; alerts jobFailed and queryThreshold. Portal page Scheduler.
  • Data feed and reports: read-only OData v4 feed /feed/v1 (14 entity sets, $select, $filter, $orderby, $top, $skip, $count, $metadata, server paging) from a whitelist translated to parameterised SQL under RLS; report designer (saved lists and grouped reports with parameters, no free SQL; preview, CSV / XLSX / PDF export, schedule); PDF composer without packages incl. an embedded TrueType font with Arabic shaping (E-RPT-012 without a font). Portal page Report designer.
  • Customer display: images uploaded on the portal (display_media, pull kind 23, SHA-256 checked on the register), idle slideshow, banners, promotions and loyalty balance of the ticket, texts and direction of the register's language; capture by the customer (signature, tip, e-mail / phone with the consent text, feedback 1 to 5) with a token per capture and "take over" on the lane; feedback as ticket event feedbackCaptured projected to ticket_feedback. Portal page Customer display.
  • Offline card policy: offlineCardMode off / standaloneVoucher / storeAndForward with floor, ticket and daily limits and schemes, enforced on the register from its local database (E-TND-010, E-TND-011) and checked again at intake (alert offlineCardException); tender flag offline; terminal log kinds offlineSale / offlineForward; alert offlineCardPending after offlineCardForwardHours; right tender.card.offline bound and always journaled. The simulator terminal stores and forwards; ZVT and OPI refuse until the device sign-off.
  • Register: terminal adapters with the store-and-forward capability, offline card sheet, receipt message panel, display controller with capture, remote action runner (postponed actions kept locally, scheduled shutdown with a warning five minutes before, message banner above every screen), remote wipe with a marker that survives a kill, localization EN / AR / FR. appVersion is now written by the version tooling (it was 0.1.0 since M0).
  • Found by the exit drill and corrected: two delivery passes could send the same webhook delivery, message or e-mail (every pass now claims its rows with FOR UPDATE SKIP LOCKED); receipt PIN mode ignored company and store profiles; a postponed restart waited five minutes for the redelivery; a receipt request could go up before its ticket; retentionSupportBundleDays had no effect.
  • Rights: admin.api.clients, admin.webhooks, admin.notifications, admin.scheduler, admin.registers.wipe, admin.reports.design, admin.reports.custom, admin.display, receipt.send.sms, receipt.send.whatsapp; tender.card.offline bound (templates updated by migrate). Persistence: migration M15_PlatformChannels with RLS on 15 tables. Errors: 35 new catalogue codes (E-API, E-WHK, E-MSG, E-ACT, E-SCH, E-FEED, E-RPT-010..012, E-DSP, E-REQ-002/003, E-RCP-004, E-TND-010/011, E-TRM-008).
  • Contracts and docs: M15 paths and schemas of openapi.yaml and the whole openapi-public.yaml are generated from the endpoint maps and records (tools/dev/m15_contract_gen.py); asyncapi.yaml describes webhook deliveries and the display channel; manual pages Platform and channels, Offline card / customer display / receipt messages (EN / AR); error appendix regenerated.
  • Tests: server 321 (architecture 4, domain 186, API 46, integration 85 with every live fact) incl. the live M15 posting test on SBODEMOGB (offline card sale, customer through the public API; both reversed); register 232. Exit drill green (prepare-m15 23, pos/tool/m15_drill_test.dart, m15-report, post-all, m15-cleanup); evidence in the spec §12. Pending hand steps of the Product Owner: WhatsApp message through the Twilio sandbox, Power BI Desktop refresh against an HTTPS server.
0.13.1
  • Desktop validation of M13 and M14 (branch m14-desktop-validation) against the live SAP landscape SBODEMOGB: every live fact that the cloud session skipped now runs, both exit drills are green, and every posted document was reversed. Fixes found on the way:
  • Provisioning: a numeric user field sends its length in EditSize (the Service Layer refuses Size with "Field size deviates from legal range"), so U_CP_Seq and the @CP_TICKETS numeric fields are created.
  • B1 documents: the sales order / quotation list no longer selects PaidToDate (invalid on those entity sets); the open amount of an order or quotation is its total. A document that is not found answers 404 with E-B1D-001.
  • Roll-up detail rows: Name of @CP_TICKETS is the ticket ULID (user tables keep Name unique), and a duplicate answer counts as written only when the row with the row's own Code exists.
  • Signatures: the Attachments2 multipart part sends Content-Disposition first with quoted name and filename (the Service Layer answered 222 "Bad Content Disposition"). A company without an attachments folder answers -5002, reported as E-SIG-002 while the document posts.
  • Store hub: the mirror (GET /registers/{id}/store-registers) carries storeSettingsJson, the store's effective hub settings (kds*, collectionCodeMode, boardKey, boardRetainMinutes, reservation*, waitlistQuoteMinutes), kept as the hub's store profile. A hub-hosted store now issues collection codes in mode code (it answered the order number) and can serve the order-ready board.
  • Terminal log: the server accepts the M13 kinds preAuthIncrement and captureAdjust (E-TRM-005 before); terminal_transactions.kind is 24 characters wide (migration M13_TerminalKinds; the 16-character kind answered 500); the reconciliation takes the latest approved capture adjustment as the amount of its capture.
  • Pay-at-table: the pay link is built from the asking register's store while the check has not reached the backend yet (hub push), so the QR is available right after the send.
  • Drills: m14_drill_test.dart follows the operator's effective right for the age check, picks items without batch / serial management and accepts Duplicate for a repeated B1 event; prepare-m13 also retires RU / PT lanes (trial seats).
  • Tests: server domain 134, architecture 4, API 28, integration 74 with all live facts; register 209. Evidence in the specs M13 §10 and M14 §9.
0.13.0
  • M14 (branch m12-inventory-j7umf0, spec docs/specs/M14_Posting_Modes_B1_Events_Spec.md, catalogue 13.7, 13.14, 13.18, 13.19, 13.21, 23.1, 23.2, decision D6) — posting modes and B1 events: roll-up posting, B1 event framework, server extensions, register hooks, signatures as B1 attachments, B1 documents into the register. The §8 proposed defaults were taken as the decisions (batch per store, aggregated invoice lines with the ticket detail in a user table, polling plus a signed webhook receiver, compiled-in register hooks, signature on the register screen, server pricing providers deferred).
  • Roll-up posting (D6 option): postingMode rollUp defers qualifying sale tickets (Deferred, no job) and rollup_batches consolidate a store's (or register's) business date into one A/R Invoice for the cash customer with lines aggregated by item × price × discount × tax × warehouse plus the incoming payments per means (roll-up-sale process, chained keys, primary DocEntry persisted first), the ticket detail written to the user tables @CP_TICKETS / @CP_TICKETLINES (provisioned by Provision fields, sap_connections.udts_provisioned_at, E-RUP-002 until then); late uploads open the next batch sequence; reversal cancels the documents, marks the rows reversed and defers the tickets again; the day close queues the batch (rollUpAt dayClose or HH:mm from the monitoring tick) and refuses on a failed one (E-DAY-005); a fiscal company cannot enable roll-up (E-RUP-001, new key fiscalProvider); alert rollUpPending; portal page Roll-up (batches, tickets, queue / repost / reverse), Deferred in Operations and Tickets; RollUpAggregator vectors in the domain tests.
  • B1 events: MasterDataSyncWorker polls every connected company (masterDataPollMinutes, default 15; full loads every masterDataFullPollHours) with a per-company lock, records each poll as a b1_events row, and processes queued events: POST /b1-events/{companyId} signed with b1EventSecret (HMAC-SHA256, timestamp within 5 minutes, E-EVT-001 otherwise) → dedupe (b1EventDedupeMinutes) → targeted SyncKeysAsync (Items by ItemCode, BusinessPartners by CardCode) or the delta, outcomes Synced / NoChange / Duplicate / Failed; alert b1EventsStale; portal Monitoring B1 sync panel, /admin/b1-events list / stats / process; SignalR masterDataChanged on /hubs/portal and the store groups; the heartbeat answer carries masterVersion and a register behind it pulls at once. The Connect Agent bridge to the B1 event service stays a documented gap (D-M14-3).
  • Server extensions: IPosExtension is now called before and after every document SapDocs.PostAsync sends (Application ports IServerExtension / IIntakeHook / IEventSourcePlugin with Infrastructure adapters, Application never references the plug-in assembly), enabled and ordered per company by serverExtensions (id, priority, settings; E-EXT-002 for an unknown id at save), a throw or timeout (extensionTimeoutSeconds) before posting fails the job non-retryable with E-EXT-001, after-post failures raise extensionFailed; ITicketIntakeHook runs once per newly stored ticket; sample plug-in plugins/CertiPos.Extensions.Sample (NumAtCard, operator in the remarks, optional U_CP_Ext, intake counter) with Api tests; /admin/extensions and the portal Extensions page.
  • Register hooks: RegisterHook contract and HookPipeline (item added, customer changed, before tender, ticket completed, buttons hook:<id>:<button>, field rules; results allow / warn / requireApproval (through the M6 authorizer, journaled) / veto (E-HOK-001 before any event); every decision recorded as a hookDecision event), compiled-in registry enabled per store by registerHooks (E-HOK-002 for a missing hook), sample age_check hook (approval for a restricted item group, "ID checked" button, tender veto without a confirmation); every tender append now goes through one appendTender choke point.
  • Signatures: SignaturePad on the register (PNG ≤ 64 KB), policy signatureCapture off / cardAbove (signatureCardAbove) / always / onAccount, complete() refuses E-SIG-001 without a signature unless tender.signature.skip is approved (journaled), drift signature_rows + event signatureCaptured + outbox kind signature (POST /registers/{id}/tickets/{ticketId}/signature, idempotent by ticket, may arrive before the ticket), receipt raster (GS v 0) with signatureOnReceipt; server ticket_signatures, attach step after every successful ticket posting (Attachments2 multipart then AttachmentEntry on the document; E-SIG-002 kept on the signature row and the job timing, the document stays posted; signature-attach follow-up job for a late signature; reversal clears the link), portal ticket detail image, e-receipt image.
  • B1 documents into the register: B1DocumentService reads open sales orders, quotations and A/R down payment invoices live through the Service Layer (/registers/{id}/b1/sales-orders, /b1/quotations, /b1/down-payments, gate b1DocumentsEnabled, rights sales.b1.orders / sales.b1.quotations / sales.b1.downpayments); the customer sheet's B1 documents list imports the open lines (document prices, baseType 17 / 23, baseEntry, baseLine, fromB1; quantities never above the open quantity, E-B1D-003) and draws a B1 down payment as a deposit tender with reference2 = B1:<docEntry>; invoice lines post with BaseType / BaseEntry / BaseLine, DownPaymentsToDraw accepts B1-origin entries, base-document rejections translate to E-B1D-002 / E-B1D-003.
  • Rights: sales.b1.orders, sales.b1.quotations, sales.b1.downpayments, sales.age.confirm, tender.signature.skip, portal admin.rollup, admin.extensions (templates updated). Configuration keys per spec §7.2 plus fiscalProvider. Alert kinds rollUpPending, b1EventsStale, extensionFailed. Contracts: roll-up batches, B1 events, extensions, signatures, B1 documents, Deferred, heartbeat masterVersion, base line fields, messages masterDataChanged / rollUpChanged. Errors E-DAY-005, E-RUP-001…005, E-EVT-001…003, E-EXT-001…002, E-HOK-001…002, E-SIG-001…002, E-B1D-001…003; manual pages "Posting modes, B1 events and extensions" (portal) and "Register hooks, signatures and B1 documents" (register) EN / AR.
  • Persistence: migration M14_PostingModes with RLS on rollup_batches, b1_events, ticket_signatures, columns tickets.roll_up_batch_id, sap_connections.udts_provisioned_at; register drift schema 12 (signature_rows).
  • Tests: server domain 133, architecture 4, API 28 (extension pipeline), integration M14FlowTests (roll-up deferral / batching / failure states, signed and rejected events, dedupe, extensions validation and the sample intake hook, signature upload rules and views, B1 endpoint gate) on the ephemeral PostgreSQL; M14PostingTests live facts (roll-up day posted and reversed, invoice based on a sales order, signature attached) skipped in the cloud; register 209 (hook pipeline, M14 controller flows, signature pad EN + RTL, B1 documents sheet EN + RTL, rights coverage). Drill prepare-m14 / pos/tool/m14_drill_test.dart / m14-report pending on the desktop with the live facts (spec §9 evidence).
0.12.0
  • M13 (branch m12-inventory-j7umf0, spec docs/specs/M13_Restaurant_Advanced_Spec.md, catalogue 20.4.3, 20.4.6, 20.2.1–20.2.3, 20.5.2, 20.5.3, 20.6.2, 11.12) — restaurant advanced: kitchen display, order-ready board, reservations and waitlist, bar tabs, tip pooling and payout, QR pay-at-table. The §9 proposed defaults were taken as the decisions (KDS device without a seat, simulator provider only, deposits through the M4 deposit, new account keys, collection code mode per store).
  • Kitchen display: kitchen_orders hosted by the backend or the store hub (one order per send × station × course, kinds order / modified / fire / void, start / ready / bump / recall / priority under an optimistic version, expo bump of a round marks the table served, recall window kdsRecallMinutes, all-day counts, speed-of-service stats); a register enrolled as platform Kds takes no licence seat and gets no update offer; register KitchenClient (create at send, actions, mirror, replay of outbox kind kitchen), KDS screen (cards with timers, bump bar, keys, all-day panel, offline banner E-KDS-002), stations in mode print / kds / both with the fallback slip when the hub is unreachable (kdsFallbackPrint, E-KDS-001); portal Menu → Kitchen (station mode, expo, sort order, board link), report /admin/reports/kitchen (stations / hours / items).
  • Order-ready board: collection codes with the order number (collectionCodeMode orderNumber or code, alphabet ACEFHJKLMNPRTUVWXY3479), ticket event collectionCodeSet, anonymous board page /board/{store}?k=<boardKey> served by the backend and the hub (codes only, boardRetainMinutes), register Board tile.
  • Reservations and waitlist (no SMS): reservations on the backend (book of record; hub mirror through store-reservations), capacity per slot (reservationSlotMinutes, reservationCapacity or the floor-plan seats, E-RSV-002 with a journaled override), holds mark the table reserved within reservationHoldMinutes, deterministic table suggestion, seat opens the table, deposit link to the M4 deposit ticket; waitlist_entries on hub or backend (positions, quotes, notify / seat / leave / requote); register host screen (timeline, book dialog with smart pickers, waitlist chips, offline queue for kinds reservation / waitlist); portal Reservations page.
  • Bar tabs: tabsEnabled, openBarTab (name or the card names it, tabPreAuthDefault, event tabNamed, order type bar, parked like a held ticket), lookup by name / last four / number, resumeTab, top-up in place on adapters with incremental authorisation (preAuthIncrement) or a new reservation with preAuthReleased, threshold tabTopUpThresholdPercent, night auto-close (tabAutoCloseTipPercent, E-TAB-003 keeps a declined tab held), tip adjustment after capture (captureAdjust, tipAdjustMaxPercent, E-TAB-004); terminal adapters expose capability flags (simulator both, OPI incremental only, ZVT and standalone neither).
  • Tips: tip_entries projected from ticket intake (card / cash tip lines per server) and from the shift close (declaredTips, right time.tips.declare), TipPoolRules (points / percent / hours / sales, tip-outs to roles, floor rounding, conservation) with the shared vector file pos/test/support/tip_vectors.json checked on both sides, tip_distributions computed on demand or from the portal (adjust with a reason, E-TIP-003 once paid), register shift screen declares tips and pays out (tipPayout drawer movement, right cash.tip.payout, posts tipsPayableAccount → cash), portal Tips page.
  • Pay-at-table: guest_payments, plug-in contract IOnlinePaymentProvider (PluginKind.OnlinePayment) with plugins/CertiPos.Payments.Simulator, Application port IOnlinePaymentGateway, anonymous guest page /p/{token} (M7 receipt token; whole, equal portion or amount, tip, provider redirect or embedded step), signed provider callback /payments/callback/{provider} (unverified callbacks answer E-PAY-002 and change nothing), checkPaid to the owning register plus /registers/{id}/guest-payments on every sync, register applies an online tender (TicketTender.paymentId) and completes the check, intake verification against the paid guest payment (E-PAY-003), posting as a transfer means on onlinePaymentAccount, portal refund through the provider, alerts tabCaptureFailed and guestPaymentUnapplied (payAtTableApplyWarnMinutes).
  • Rights: fnb.kds.open, fnb.kds.bump, fnb.kds.recall, fnb.kds.expo, fnb.board.open, fnb.reservations.view, fnb.reservations.edit, fnb.waitlist, fnb.tab.open, fnb.tab.topup, fnb.tab.others, fnb.tab.autoclose, fnb.tip.adjust, fnb.paytable.qr, portal admin.reservations, admin.tips; time.tips.declare and cash.tip.payout bound (register 139, portal 22, later 25); new template KITCHEN; migrate adds the new rights to existing template roles. Configuration keys per spec §8.2 (kds*, collectionCodeMode, boardKey, boardRetainMinutes, reservation*, waitlistQuoteMinutes, tabs* / tab*, tipAdjustMaxPercent, tipPoolRules, tipsPayableAccount, payAtTable*, onlinePaymentAccount); kitchen station definition gains mode, expo, kdsSortOrder.
  • Contracts: register and admin paths for kitchen, reservations, waitlist, tips, guest payments and the pay link, public board / pay / callback paths, schemas KitchenOrder, Reservation, WaitlistEntry, TipEntry, TipDistribution, GuestPayment, PayPage; TableView.status gains reserved, TableStateView gains kitchen / waitlist, TicketSnapshot gains tabName / collectionCode and order type bar, TicketTender.kind gains online with paymentId, ShiftUpload.declaredTips, DrawerMovementUpload.kind tipPayout; messages kitchenChanged, waitlistChanged, reservationChanged, checkPaid. Error codes E-KDS-001…006, E-RSV-001…005, E-TAB-001…005, E-TIP-001…004, E-PAY-001…005; manual page "Kitchen display, reservations, tabs and tips" EN / AR; error appendix regenerated.
  • Persistence: migration M13_RestaurantAdvanced with RLS on the six new tables, columns tickets.tab_name / collection_code, ticket_tenders.payment_id, shifts.declared_tips_json; register drift schema 11 (kitchenOrderRows, reservationRows, waitlistRows, ticket head tabName / collectionCode, shift declaredTipsJson).
  • Tests: server 218 (domain 128 incl. M13 domain invariants and vectors, architecture 4, API 22, integration 64 with M13FlowTests; 16 live facts skipped in the cloud), register 186 (m13_checkout, tip_pool_rules, kds, kds_screen, host, host_screen incl. RTL). Fixed on the way: the promotions test is scheduled off today's weekday, the ZVT / OPI simulators tolerate a dropped socket, the outbox uploader skips the client-replayed kinds. Pending on the desktop (rule 1, no SAP in the cloud): M13PostingTests live posting with reversal and the prepare-m13 / m13_drill_test.dart drill (spec §10 evidence).
0.11.0
  • M12 (branch m12-inventory, spec docs/specs/M12_Store_Inventory_Spec.md, catalogue 7.1–7.5, 7.7, 7.8, 7.12, 13.5, 13.10, 16.10) — store inventory and labels.
  • Server: inventory_documents (register documents with posting state, park reason, SAP DocEntries), stock_levels (cache of ItemWarehouseInfoCollection refreshed by the InventoryWorker every stockRefreshMinutes, after every succeeded inventory posting, on demand from the portal or a register), kits (product trees of type production / assembly synced as master data, sync entity ProductTrees, pull kind 22); migration M12_Inventory with RLS. InventoryService intake (idempotent by ULID, rule failures park, never 4xx on a replay), StockService (store rows, live item figures, open purchase orders and transfer requests read live from the Service Layer), portal list / detail / void, inventory reports status / movements / sellers / abc; register endpoints under /registers/{id}/inventory-documents, /stock, /purchase-orders, /transfer-requests (tag inventory, also the CertiScan handheld contract, docs/CertiScan_Inventory_API.md).
  • Posting: one process per kind — inv-goods-receipt (InventoryGenEntries), inv-grpo (PurchaseDeliveryNotes based on the order, BaseType 22), inv-vendor-return (PurchaseReturns, based on the GRPO), inv-transfer-request (InventoryTransferRequests), inv-transfer (StockTransfers, through transitWarehouse when set, based on the request), inv-count (InventoryCountings then InventoryPostings for the varied lines, the counting closed afterwards), inv-adjustment and inv-kit (InventoryGenEntries / InventoryGenExits, the first DocEntry persisted before the second); serials by SystemSerialNumber on issues, batches with expiry. Exactly-once creates: a B1 document already carrying the CertiPOS marker is adopted; a -2028 answer with a draft behind it is the new E-INV-012 (SAP approval procedure; approve the draft, the job adopts it on retry). Reversal: InventoryPostings mirrored by a second inventory posting back to the book figure, InventoryGenEntries / InventoryGenExits mirrored when the Service Layer refuses Cancel ("Invalid entityset", SL 1000330), InventoryTransferRequests closed; partial timings (incl. the draft number) survive a failed job. Serials / batches captured at a sale post on the invoice line.
  • Register: drift schema 10 (inventory_doc_rows, stock_rows, kit_rows), InventoryRules mirror (E-INV-002 / 003 / 007 / 009, negative-stock rule, kit expansion), InventoryController (drafts that survive a restart, completion validates, checks the kind's right, inventory.count.post on a variance, journaled adjustments, stages outbox kind inventory in the same transaction), hub screen (tiles per right, stock check with live refresh, document list), one editor for every kind (warehouses, vendor, reasons, direction, purchase order pre-fill, kit expansion, serial / batch capture dialogs, one primary action), labels at receiving (right inventory.labels.receiving, copies = quantity), stock pulled on every sync, kits on the pull, checkout addItem applies negativeStockPolicy (block E-INV-001 unless inventory.negative.override approves, warn badge, allow) and accepts serials / batches; menu tile Inventory; EN / AR / FR strings.
  • Rights: area inventory — inventory.open, .stock.view, .receive, .receive.baseless, .transfer.request, .transfer.ship, .transfer.receive, .vendor.return, .count, .count.post, .adjust, .kit, .labels.receiving, .negative.override, portal admin.inventory (register 105, portal 20, later 37); templates updated (store manager everything, supervisor without count post / override, senior cashier receive / transfer / count / stock, cashier stock view). Configuration keys negativeStockPolicy, allowedWarehouses, transitWarehouse, stockRefreshMinutes, adjustmentReasons, receivingLabels; error codes E-INV-001…012; portal page Inventory (stock, documents, reports); manual page "Store inventory and labels" EN / AR; contracts (paths, schemas, MasterDataPull.kits, TicketLine.serials / batches).
  • Tests: server (M12 domain rules, M12 flows without SAP, live posting of every kind to SBODEMOGB with reversal — the demo's active "Goods Issue" approval template turns the goods-issue paths into E-INV-012 evidence), register (rules, controller flows for every kind, checkout policy, hub / editor widget tests EN + RTL). Drill prepare-m12 / pos/tool/m12_drill_test.dart / m12-report / post-all (see the spec §8 evidence).
0.10.0
  • M11 (branch m11-loyalty-giftcards, spec docs/specs/M11_Loyalty_GiftCards_Spec.md, catalogue 4.3, 4.9–4.12, 4.16, 4.17, 13.6, 16.4 and the store-credit tender deferred from M4) — loyalty, gift cards, store credit.
  • Stored value on the server (value_accounts + value_ledger): gift card types (fixed amounts or a range, expiry, reloadable, cash-out mode and cap, redeemable item groups / stores / weekdays, customer binding, number prefix), pre-printed / digital cards generated in bulk with CSV, lookups by number or customer, block / unblock / adjust (audited), expiry with one breakage journal; store credit vouchers opened by a return refunded to store credit and redeemed by any register of the customer. Rules broken after the sale are applied, flagged on the ledger entry and raised as the alert valueAccountException; the ticket is never parked.
  • Loyalty on the server (loyalty_plans, loyalty_members, loyalty_ledger): plans with currency or visit accrual, point value and redemption limits, tiers by lifetime points, bonus periods, exclusions, scope and inactivity expiry; members with a CPL… member number (card / QR); enrolment from the portal or the register (outbox kind loyalty, idempotent); accrual and redemption projected at intake (returns claw back), alert loyaltyException; members ride the versioned pull (kind 21) so every register sees the balance as of its last sync.
  • Register: pseudo item GIFTCARD lines (sell / reload / issue without charge / cash-out as a return line), tenders giftCard, storeCredit, loyalty, refundStoreCredit, event loyaltySet with the plan rule captured for the earn preview, snapshot loyalty block and per-line gift card fields; checkout_m11.dart with the online lookups and the offline policies (giftCardOfflinePolicy deny / approval / allow capped by giftCardOfflineMax and the cached balance, loyaltyOfflinePolicy); gift card sheet, loyalty sheet (scan a member card, enrol), tender buttons, receipt lines (masked numbers, voucher number in full, points earned and balance); drift schema 9; shared accrual vectors test/support/loyalty_vectors.json checked on both sides.
  • Posting: gift cards post as the configured non-inventory item (giftCardItemCode, giftCardTaxCode) inside the sale invoice; gift card, store credit and points redemptions are transfer means on giftCardLiabilityAccount / storeCreditAccount / loyaltyLiabilityAccount (the M7 extra-means mechanics, now also on on-account part payments and exchange settlements); a store credit refund moves the credit from A/R to the store credit account through the outgoing payment; new posting subject liability_postings with the process liability-journal (loyalty accrual: expense → liability; loyalty / gift card expiry: liability → breakage), reversible like any job.
  • Portal: pages Loyalty (plans designer, members with ledger and adjustments, enrol, accrual journal per date, expire idle points, postings with job state) and Gift cards (types, bulk generation + CSV, card search with ledger, block / adjust, outstanding liability, expire due cards); report group-bys giftCard and loyalty; migrate adds the new template rights to existing tenants.
  • Rights bound now: tender.giftcard, tender.giftcard.cashout, tender.storecredit, tender.loyalty, returns.refund.storecredit; new tender.giftcard.sell, tender.giftcard.issue, tender.giftcard.offline, tender.loyalty.offline, customer.loyalty.enrol, portal admin.loyalty (register 91, portal 19, later 37). Configuration keys giftCardsEnabled, giftCardItemCode, giftCardTaxCode, giftCardLiabilityAccount, giftCardBreakageAccount, giftCardOfflinePolicy, giftCardOfflineMax, storeCreditAccount, loyaltyEnabled, loyaltyLiabilityAccount, loyaltyExpenseAccount, loyaltyBreakageAccount, loyaltyOfflinePolicy; quick-layout functions giftCard, loyalty; error codes E-GFT-001…009, E-STC-001…003, E-LOY-001…006, E-LIA-001…003; the alert kind column widened to 40 characters; manual page "Loyalty, gift cards and store credit" EN / AR; contracts for the pull, the ticket snapshot and the new endpoints.
  • Tests: server (M11 domain invariants and vectors, M11 flows across two registers, live posting of a gift card sale, a card redemption, a store credit refund and the accrual journal to SBODEMOGB with reversal), register (loyalty rule vectors, six checkout M11 flows). Drill prepare-m11 / pos/tool/m11_drill_test.dart (two lanes) / m11-report / post-all / post-liability (see the spec §8 evidence).
0.9.0
  • M10 (branch m10-promotions, spec docs/specs/M10_Promotions_Spec.md, catalogue 5.2, 5.3, 5.4, 5.10, 4.4, 11.5, 15.7, 16.4) — pricing and promotions.
  • Promotion engine on the register, offline and deterministic (PromotionEngine, vector file test/support/promotion_vectors.json): percent off, amount off, fixed price and N-for-N, buy X get Y with the lowest-value rule and reward sets, ticket thresholds with percent / amount / free item, quantity breaks; targets (items, groups, all) with exclusions, scope (stores, order types, customer groups), schedule (dates, weekdays, time window), per-ticket and soft total limits, priority, stackable, markdowns with reason and "was" price. Manual and promotional discounts resolve per discountResolution (sum / max); the effective values stay what the server verifies (E-TKT-004 unchanged).
  • Ticket events promotionsApplied (written when tendering starts), promotionRemoved (right pricing.promotion.remove, journaled), couponApplied / couponRemoved; reward lines for free items; snapshot fields promotionVersion, coupons, per line promotionCode, promoPercent, promoAmount, wasPrice, rewardLine, couponCode.
  • Coupons: campaigns per company (shared code checked on the register even offline; unique single-use codes generated in bulk, CSV, verified online through POST /registers/{id}/coupons/validate), offline policy couponOfflinePolicy (deny E-CPN-005 / approval with pricing.coupon.offline / allow), redemptions recorded at intake with exceptions (E-CPN-006 over-use, expiry, customer binding) that never park the ticket but raise the new alert kind couponException.
  • Server: tables promotions, coupon_campaigns, coupon_codes, coupon_redemptions, ticket_promotions (migration M10_Promotions with RLS), pull kinds promotions / couponCampaigns, admin endpoints under /admin/promotions, /admin/coupon-campaigns, /admin/coupon-redemptions, portal pages Promotions (designer with the item smart field, applications list) and Coupons (campaigns, code generation, redemptions with exceptions), report group-by promotion; the discount column of the sales reports now counts percentage discounts.
  • Posting: rewards ride the existing line discounts (free items as 100 % lines); new user fields U_CP_Promo on INV1 / RIN1 and U_CP_Coupon on OINV / ORCT, written once Provision fields has run again after this upgrade (UdfProvisioningService.PromoUdfsSince); provisioning now logs the pooled Service Layer session out so the new fields are usable without a restart.
  • Upgrade path: migrate synchronises the template roles of every tenant with the current catalogue (adds rights newer builds introduced, never removes), so administrators of existing tenants see the new pages; new rights pricing.coupon.apply, pricing.coupon.offline, pricing.promotion.remove, admin.promotions (register 81, portal 18, later 42).
  • Configuration keys discountResolution, couponOfflinePolicy, promotionsEnabled; quick-layout functions promotions, coupon; error codes E-PRO-001…004, E-CPN-001…007; manual page "Promotions and coupons" EN / AR; contracts for the pull, the ticket snapshot and the new endpoints.
  • Tests: server 186 (104 domain incl. promotion / coupon invariants, 4 architecture, 22 API, 56 integration incl. M10 flows, template sync and the live posting), register 134 (24 engine vectors, 5 checkout promotion flows). Drill prepare-m10 / pos/tool/m10_drill_test.dart / m10-report (see the spec §8 evidence).
0.8.1
  • Portal fix (found by the SAPW pilot: "Add company" did nothing and showed no error): the host never served
  • Portal: the host picks the render mode per page (AcceptsInteractiveRouting): the sign-in and first-run pages stay
  • Portal: standard Blazor error banner (reload / dismiss) so an unhandled circuit error is never silent again.
  • Tests: PortalInteractivityTests (script served, pages reference it and carry the banner, hub negotiates, packaged
  • Installer: CertiPOS-Server-0.8.1.exe rebuilt with the fix; run it over an existing installation (repair keeps data
0.8.0
  • M9 (branch m9-hardening-installers) — hardening: ASVS L2 controls, resilience, performance budgets, RTL pass, NSIS installers end to end, pilot cut-over runbook (spec docs/specs/M9_Hardening_Installers_Spec.md).
  • Security (R-17.3): sliding-window rate limits per client address (20 / min on login, register token, enrolment and the portal form; 600 / min on the API; 429 with E-SEC-001 and Retry-After), security headers on every response (nosniff, DENY framing, no-referrer, permissions policy, COOP, CSP frame-ancestors / base-uri / object-src / form-action, no-store on /api, HSTS over HTTPS), HTTPS listener with the server's own ECDSA certificate (cert init) or a customer PFX, certificate fingerprint published on /meta and in the enrolment QR (&f=), the register pins it (E-NET-002 on mismatch), protected secrets.json (DPAPI / AES-GCM) loaded as configuration, request body ceiling 16 MB (1 GB on build uploads), docs/security/ASVS_L2_Checklist.md with the open gaps named.
  • Observability (R-01.8): Serilog rolling file under the data directory (14 days), server support bundle zip (logs, masked configuration, versions, health, migrations, counts) from Maintenance, register rotating log (7 files) and text support bundle sent from the Device page and listed on the register page.
  • Resilience (R-12.1, R-12.4, R-12.7): store key offlineCheckInDays refuses sign-ins past the check-in deadline (E-SYN-001) until a sync succeeds; the heartbeat answers serverTimeUtc, the register reports clockSkewSeconds, Monitoring shows the skew and flags it beyond clockSkewToleranceSeconds, the register shows a warning bar (E-SYN-002); error-injection suites on both sides (network drop mid-tender, power loss mid-ticket, hub loss mid-table-service, Service Layer outage during posting proven live and reversed).
  • Performance budgets (R-17.1, R-17.2): register scan-to-line p95 and tender completion measured on a file-backed database (8.9 ms / 34.9 ms locally against 150 ms / 1 s), server 200 registers heartbeating and uploading 400 tickets (acknowledgement p95 29 ms, 505 tickets / s locally against 2 s / 20 per s); both print PERF lines and assert with a 2× CI allowance.
  • RTL pass (R-17.4): every register screen renders under Arabic with LTR islands for codes; new tests for enrolment, menu, device, peripherals, shift, time clock and splash.
  • Installers (D2, R-01.4, R-01.5, R-01.6): server installer with role / settings / hub pages and silent switches, portable PostgreSQL bundle, initdb + service + provision-db, protected secrets, cert init, migrate, CertiPOS service with recovery and dependency, daily backup task, readiness wait, repair keeps data and configuration, uninstall with /DROPDATA=1; register installer with enrolment page, pending-enrolment.json for the app's first start, /UPDATE mode used by the register's new "Install now"; build-installers.ps1 with SHA-256 sidecars, build-apk.ps1, drill-installers.ps1 + drill-clean.ps1 (silent install → smoke → repair → uninstall, elevated; green on the build machine for both installers on 2026-09-24).
  • First-run page /setup in the portal (no default credentials: the sign-in page redirects there until the first organisation and administrator exist; E-SET-002 on password mismatch), found by the first pilot install.
  • One binary, more commands: backup (with pruning), provision-db, secrets set / generate / ensure / list, cert init, check; the service hosts itself under the Service Control Manager.
  • Documentation (R-17.5): docs/Installer_Guide.md, docs/runbooks/Pilot_Cutover_Runbook.md, manual pages (install rewritten EN + AR, updates / backups / support EN).
  • Migration M9_Hardening (register clock_skew_seconds); error catalog E-SEC-001, E-NET-002, E-SYN-001 / 002, E-UPD-004, E-SUP-001, E-INS-001 / 002; contracts for /meta, the heartbeat and the support-bundle endpoints.
  • Tests: server 167 (93 domain, 4 architecture, 18 API incl. security headers, rate limit, secrets precedence and the first-run page, 52 integration incl. M9 flows, host commands and the first-run page on fresh clusters, perf budget, live Service Layer outage), register 105 (certificate pinning against a local TLS server, error injection, perf budget, RTL pass, check-in deadline, pending enrolment, update banner).
  • Not in this build: nonce-based CSP, portal MFA, requireTls switch, read auditing, penetration test (all listed in the ASVS checklist), MSIX, iOS ABM packaging, Docker (D2), remote wipe, the Connect Agent role (the installer refuses it with E-INS-001).
0.7.0
  • M8 (branch m8-portal-completion) — portal completion: monitoring with remote actions, alerts by e-mail and in-app, reports with CSV / XLSX, POS vs B1 reconciliation read live from SAP, software update channel with staged rollout, audit export and configuration-change log, configuration export / import, backup and restore, day close, user manual EN / AR (spec docs/specs/M8_Portal_Completion_Spec.md).
  • Monitoring (R-15.1, R-12.6): the heartbeat carries oldestPendingAtUtc, drawerCash and platform; registers store pending uploads, backlog age, drawer cash and a requested remote action; the Monitoring page shows online / offline, last seen, version, backlog, operator, drawer cash, peripherals and open alerts with Force sync, Lock and Force logout; the heartbeat answer carries action and update.
  • Alerts (catalog 15.14): alerts table, pure evaluation of offline (escalates to critical at 4×), backlog, posting failures (failed jobs and parked tickets) and low cash from the tenant rules (alertOfflineMinutes, alertBacklogRows, alertBacklogMinutes, alertLowCash, alertPostingFailures, alertRecipients, alertNotifyResolved); MonitoringWorker every 60 s raises, updates, resolves and queues e-mails through the outbox; Alerts page with acknowledge / resolve and the app-bar bell.
  • Reports (R-15.3): sales by store, register, cashier, hour, item, category, tender, salesperson or order type; shifts and cash reconciliation with the Z archive (the register's stored report plus drawer movements); every report as JSON, CSV (UTF-8 BOM) or XLSX from a dependency-free SpreadsheetML writer; POS vs B1 reconciliation reads invoices and credit notes live through the Service Layer and matches them to tickets by the document comment (matched, amountDiffers, notInB1, notInPos, cancelledInB1, reversed).
  • Updates (R-15.2): register_builds per platform and channel with SHA-256, multipart upload, publish / unpublish / delete, store-scope updateChannel and updateRolloutPercent, stable rollout buckets per register, offer in the heartbeat when a newer published build applies, download under the register bearer; the register shows the offer on the Device page and downloads it with checksum verification (E-UPD-002).
  • Audit and configuration (R-15.4, R-15.5): audit CSV export, configuration-change log with before / after (profiles now audit before), key-level diff in the portal; configuration bundle export / import keyed by codes with dry run and apply (certipos-config/1); pg_dump backups from the portal (CertiPos:Backup:ConnectionString for a role that bypasses RLS) and the CertiPos.Api restore <file> command; day close per store and business date (E-DAY-001 open shifts, E-DAY-002 unposted unless force, E-DAY-003 already closed) with forced clock-out, stored summary, late-ticket count and a summary e-mail; cash.day.close, admin.monitoring and admin.updates leave the later set.
  • Migration M8_Portal with RLS on 3 tables (alerts, register_builds, day_closes), register columns, ticket sales_employee_code; endpoints and contracts for monitoring, alerts, reports, audit, config, backups, updates, day close; error catalog (E-ALT, E-ACT, E-UPD, E-DAY, E-BKP, E-CFG-005, E-RPT-002).
  • User manual (R-17.5): Astro Starlight site in /manual (EN root, AR RTL) with getting started, register and portal chapters and the error appendix generated from docs/contracts/errors.md (tools/manual/gen_errors.py); CI job manual builds it.
  • Tests: server 150 (93 domain incl. 4 M8, 4 architecture, 9 API, 44 integration incl. 8 M8 flows: monitoring and remote actions, alerts raised / e-mailed / resolved, reports with CSV and XLSX opened back, bundle round trip, backup + restore into a second database, update offers and download, day close, live POS vs B1), register 86 (update banner EN + RTL, offer parsing).
  • Exit drill passed live (pos/tool/m8_drill_test.dart, m3_drill_server.py prepare-m8 / m8-report / post-all): lane online on Monitoring, backlog and low-cash alerts raised while selling offline and resolved after the upload, offline alert raised after a minute without heartbeat and acknowledged, remote lock consumed by the heartbeat, reports and the Z archive, POS vs B1 matched the three drill tickets live, an update build offered and downloaded with a matching checksum, configuration exported and re-imported, a backup written, day close refused for open shifts, audit exported; postings reversed.
  • Not in this build: SMS / WhatsApp / push notifications, Power BI / OData feed, report and dashboard designers, scheduler beyond the workers, restart / wipe remote actions, PDF summaries, period-end closings, scheduled backups, silent register installation (M9).
  • Repository published to GitHub (Waelorna/CertiPOS) with the product README; CI now passes the strict Release build (warnings as errors: analyzer fixes, MudBlazor 9 attribute names, .NET 10 pruned package references) and generates the drift code before analyzing the register; the live POS vs B1 test reverses the ticket it posts during its reversal pass.
0.6.0
  • M7 (branch m7-terminals-payments) — terminals and payments: card terminal adapters, terminal transaction log with server reconciliation, cheque / bank transfer / custom tenders posted to B1, customer display, item labels, e-receipts and the digital receipt page, peripherals tests reported with the heartbeat (spec docs/specs/M7_Terminals_Payments_Spec.md).
  • Register terminals: TerminalAdapter contract (sale, refund, void, pre-auth, capture, settle, ping, status; results never carry more than the last four digits) with four adapters — standalone (manual capture), simulator (scripted by the amount's cents), ZVT ECR protocol over TCP (registration, authorisation, reversal, refund, pre-authorisation, book total, end of day, repeat receipt, abort; APDU framing, BMP parsing, print blocks kept as receipt text) and OPI / IFSF card service (length-prefixed XML, device requests answered, Repeat for recovery). TenderTerminalService writes every operation to terminal_transactions before the send (drift schema v7), completes it with the outcome and stages it in the outbox (kind terminal, batched); pending or timed-out rows are resolved at start-up through the adapter's status / repeat receipt. Tender screen: card on the terminal (approved → tender with approval code, terminal reference, scheme, last four; declined / cancelled → message; timeout → retry with the same key or manual capture), void through a device reversal, refund on the terminal for returns, Open tab / Capture tab (pre-authorisation on checks, released on void), Close terminal batch on the shift screen.
  • Tenders: cheque (number, bank), bank transfer (reference) and configured custom tenders (tenders: code, label, glAccount, treatAsOnAccount, informationOnly, openDrawer, maxAmount, changeLimit, refundable, receiptCopies, stores) with the guards E-TND-005 (maximum), E-TND-006 (change), E-TND-007 (not refundable), E-TND-008 (definition); banknote quick buttons from cashBanknotes; receipt labels for every kind. Posting: cheques as PaymentChecks with CheckAccount = chequeAccount, transfers as TransferSum on transferAccount, custom tenders on their G/L account — a second incoming payment when a ticket carries a second transfer account (E-PST-016 beyond that), on-account custom tenders leave the invoice open. Proven live on SBODEMOGB: cheque + transfer + voucher ticket → invoice + payment (cheque and transfer) + payment (voucher), all reversed.
  • Server: terminal_transactions intake (idempotent batches, PAN guard E-TRM-005) and store reconciliation per register and business date (orphan approvals, unmatched tenders, amount differences, voids matched by terminal reference, settlements); e-receipts (email_outbox, MailKit SMTP from the tenant settings smtpHost, smtpPort, smtpUser, smtpPassword, smtpFrom, smtpStartTls, back-off up to 5 attempts, EmailWorker every 15 s, portal retry), HMAC receipt links (receiptBaseUrl + /r/{token}) and the public digital receipt page; labels rendered from master data (ZPL / EPL, 50x30 / 40x20) for download or raw TCP printing from the server; heartbeat peripherals stored on the register; migration M7_TerminalsPayments with RLS on 2 tables (text columns for e-mail bodies and receipt text).
  • Endpoints: register terminal-transactions, tickets/{id}/ereceipt, tickets/{id}/receipt-link; portal terminal-transactions, terminal-reconciliation, labels/render, labels/print, tickets/{id}/ereceipt, tickets/{id}/receipt-link, email-outbox (+ retry, send-due); public /r/{token}; contracts (tender kind pattern incl. cheque, transfer, custom:<CODE>, heartbeat peripherals) and error catalog (E-TRM, E-TND-005..008, E-LBL, E-RCP, E-PST-016).
  • Portal: Terminals (log per store / date with reconciliation, settlements, CSV), E-mail outbox (status, retry, send now), Items → labels (download ZPL / EPL or print to a network printer), Ticket page → e-mail receipt and digital receipt link, Register page → peripherals.
  • Register: customer display server (customerDisplayPort, self-contained page + server-sent events mirroring the checkout; URL on the device page), label composer byte-exact with the server + Print label on the catalogue card (labelPrinter, labelFormat, labelSize), e-mail receipt on the receipt screen (right receipt.email; online straight to the server, offline through the outbox kind ereceipt) with the digital receipt link, Peripherals screen (right device.peripherals.test: receipt printer test page, drawer kick, label test, terminal ping, display test, scanner field) whose statuses ride on the heartbeat; EN / AR / FR.
  • Fixes found by the drills: Register.Retire clears the active operator (a retired lane blocked the next login for 3 minutes), ZVT session close no longer waits for the packet stream, void rows keep the reversed sale's reference for reconciliation, post-all waits for the worker on reversals.
  • Tests: server 138 (89 domain incl. 5 M7, 4 architecture, 9 API, 36 integration incl. 6 M7 flows: intake + reconciliation, e-receipt through a local SMTP sink and the public page, labels to a raw sink, heartbeat peripherals, live cheque + transfer + voucher posting and reversal), register 83 (ZVT and OPI conformance suites against in-process simulators, terminal / tender controller drills incl. power-loss recovery, tender and peripherals widget tests EN + RTL, customer display server, label golden).
  • Exit drill passed live (pos/tool/m7_drill_test.dart, tools/dev/m3_drill_server.py prepare-m7 / m7-report / post-all): ZVT adapter over TCP to the simulator — sale, void, sale, refund on a return, pre-auth + capture, settlement; terminal log uploaded and reconciled balanced on the server (3 approved, 1 voided); cheque + transfer + voucher ticket; e-mail receipt delivered to the drill SMTP sink by the server outbox and the digital receipt page opened; ZPL label to a socket sink; peripherals on the register page; all four tickets posted to SBODEMOGB and reversed.
  • Not in this build: vendor-specific terminal adapters (Geidea, N-Genius, Adyen — ZVT / OPI cover ECR-capable devices; others arrive as plug-ins after a sign-off with the device), Bluetooth / USB peripheral transports, WhatsApp / SMS receipts (M8), a native second-screen app (any browser on the LAN is the display).
0.5.0
  • M6 (branch m6-authorizations) — authorizations complete: override matrix, approvals by PIN / badge / remote, elevation per action / ticket / session, permission-usage journal with server verification, exception reports, segregation of duties, password / PIN policy, session policy, training mode, clock in / out (spec docs/specs/M6_Authorizations_Spec.md).
  • Server: right_policies (scope-merged policies per right: elevation scope, approval methods, reason required, journal), approval_requests (remote approvals decided from the portal with E-APR-003..005 rules, approvalDecided pushed to the register group on /hubs/registers), permission_events (append-only journal; intake re-verifies every approval against current rights and flags approver = performer, missing approve.register, insufficient rank or threshold without ever rejecting), credential_history (reuse rule), time_entries (clock in / out / breaks, store state, forced clock-out and a day-end worker), register active operator / force logout, badge credentials; migration M6_Authorizations with RLS on 5 tables; tenant security policy keys (pinMinLength, pinHistory, pinExpiryDays, passwordMinLength, passwordHistory, passwordExpiryDays, lockoutAttempts, lockoutMinutes, forceChangeAfterReset) and session keys (actionAfterPosting, concurrentLogins, lockTakeover, requireClockIn, forceClockOutAtDayEnd, dayEndTime, overrideReasons, remoteApprovalTimeoutSeconds, tenderPolicies, discountCodes, journalRights); operator sync carries policies, security policy, badge hash and PIN expiry; salesperson posts as SalesPersonCode; training tickets are refused on intake (E-TKT-017); void / reverse of postings need returns.postvoid / returns.reversal; store managers approve remotely.
  • Endpoints: register permission-events, approvals (request / poll / cancel), time-entries, time-state, operators/{id}/pin, session, heartbeat with activeUserId → forceLogout; portal policies (+ effective), security-policy, approvals (+ decide), permission-events, exceptions/{kind} (voids, refunds, noSale, priceOverride, discounts, postVoid, sodViolations, failedLogins), time-entries, stores/{id}/time-state, stores/{id}/force-clock-out, users/{id}/badge, registers/{id}/force-logout; contracts and error catalog (E-APR, E-SES, E-TIM, E-POL, E-PRM, E-RPT, E-USR-014..016, E-RGT-006).
  • Portal: Override matrix (per scope, one row per right, effective view), Approvals (pending cards with approve / reject and note, refreshes every 3 s, usable on a phone), Permission journal (filters, CSV), Exception reports (tabs, CSV), Time clock (store state, entries, forced clock-out), badge assignment on the user page, active operator and force logout on the register page.
  • Register: one authorizer for every action (denied → journal + refusal; approval required or over threshold → manager approval by PIN, badge or remote per policy, with reason codes; ticket / session elevations that end with the ticket or the session; per-object approvals for tenders and discount codes), permission journal mirrored through the outbox in batches, badge login, single-login rule with take-over prompt, lock screen with idle lock / logout and manager take-over, action after posting, training mode (no sequence, never staged, receipt and kitchen banners, excluded from the shift), time clock screen with requireClockIn gate, PIN change on expiry or reset, no-sale drawer kick, X / Z print, blind close, over / short threshold, others' shifts and tickets, age-check override, comp, discount codes, ticket amount discount, salesperson, held-ticket delete, cash limit, per-line refund threshold, search / price-check / customer rights; drift schema v6; EN / AR / FR.
  • Every right in the Phase 1 catalogue is now bound: 78 on the register (asserted by rights_coverage_test.dart against the server catalogue), 17 on portal endpoints (asserted by RightsCoverageTests), 42 listed as later in the catalogue with the milestone they arrive in (gift cards, loyalty, store credit, orders, terminals, tax overrides, day close, reports, data protection).
  • Tests: server 127 (93 domain incl. 9 M6, 4 architecture, 9 API incl. rights coverage, 30 integration incl. 6 M6 flows), register 57 (authorizer drills, lock screen EN + RTL, rights coverage, plus the M3–M5 suites under the authorizer).
  • Exit drill passed live (pos/tool/m6_drill_test.dart, tools/dev/m3_drill_server.py prepare-m6 / m6-report / post-all): single-login take-over across two lanes, PIN / badge / elevated / remote approvals journaled offline and online, training ticket never staged, forced clock-out, exception reports, 17 journal rows with 0 flags; the drill's sale, return and second sale posted to SBODEMOGB first attempt and were reversed.
  • Not in this build: dual control (two approvers), corporate SSO and fingerprint on the register, job-code login, native push for remote approvals (the portal page polls), scheduling / labour cost.
0.4.0
  • M5 (branch m5-restaurant) — restaurant and quick service: register modes, floor plan and shared table state on a store hub, modifiers and combos, seats and courses with hold / fire, kitchen printing by station with backup, split bills, service charge, tips, order types and order numbers (spec docs/specs/M5_Restaurant_Spec.md).
  • Server: menu master data (modifier groups, combos, kitchen stations) and store floor plans pulled by registers like every master row; shared table state (table_sessions, table_checks with event hand-over, order_counters, item_availability) with optimistic versions (E-TBL-001..008), SignalR /hubs/registers per store; migration M5_Restaurant with RLS on 8 tables.
  • Store hub role (R-01.6, R-01.7): the same binary with CertiPos:Role=StoreHub and its own PostgreSQL enrols upstream as a Hub register (no licence seat), mirrors the store's registers, keys and floor plan, validates device assertions locally, serves the table endpoints on the LAN and pushes its state upstream every 5 s; registers fall back to their own tables when the hub is down and replay queued operations when it is back.
  • Posting: service charge and tip pseudo-lines post as configured non-inventory items with TaxCode + VatGroup (live finding: VatGroup decides the line tax), combo components never post, order number / table / order type in the document comment and U_CP_OrderNo / U_CP_Table / U_CP_OrderType UDFs; verified live on SBODEMOGB (cash and on-account restaurant tickets posted and reversed).
  • Portal: Menu page (modifier groups with modifiers, combos, kitchen stations), Floor plan designer per store (rooms, sections with service charge, tables on a 40×40 grid), live Tables page.
  • Register: ticket model with seats, courses, modifiers priced into the line, combo components, SVC / TIP pseudo-lines, order type and table header (schema v5); floor plan screen with status tiles, covers prompt, pick-up / take-over, long-press actions and hub-offline banner; modifier and combo dialogs; restaurant panel on the lane (order type, seat / course, send F7, fire, split by seat or lines, pro-forma, service charge); tender tips and even-split portions with the order-only guard; kitchen slips (ORDER / FIRE / VOID) per station with backup printer; receipts with modifiers, order and table; shift report with tips and service charges per server; EN/AR/FR.
  • Fixes found by the drills: licence seats count enrolled devices only; store hub pulls register keys on demand after a failed assertion; same-second outbox rows upload in chain order; same server continuing a table elsewhere moves it without take-over.
  • Tests: server 100 (75 domain, 4 architecture, 7 API, 24 integration incl. table flows with two registers, hub push, live restaurant posting), register 47 (controller drills incl. hub outage replay and pick-up, floor / check widget tests EN + RTL).
  • Exit drill passed live: three registers on one store hub (pos/tool/m5_drill_test.dart, tools/dev/m3_drill_server.py prepare-m5 / post-all) incl. hub outage and reconciliation; all tickets posted to SBODEMOGB and reversed.
  • Not in this build: KDS screens, reservations / waitlist, bar-tab pre-auth (M7), tip pooling, pay-at-table (Phase 2), portal warning for batch / serial combo items.
0.3.0
  • M4 (branch m4-returns-payments) — returns and exchanges, on-account sales with credit check, customer deposits (down payment invoices) drawn by later sales, collections on open invoices, cash-management postings, customer quick-create to SAP (spec docs/specs/M4_Returns_Payments_Spec.md).
  • Server: ticket kinds sale | return | exchange | deposit | collection, signed return lines with returnable-quantity enforcement (E-TKT-009 parks), tender kinds account | deposit | refundCash | refundCard, customer requests with provisional CP- codes resolved at posting time (E-PST-015 waits), financial types on drawer movements, customer_requests table with RLS (migration M4_ReturnsPayments).
  • Posting processes, every one posted live to SBODEMOGB and reversed: return (A/R Credit Memo based on the open invoice or free lines, Outgoing Payment refund), exchange (credit memo + invoice netted in one payment), on-account-sale (invoice on terms, part payment), deposit (A/R Down Payment Invoice + payment), sale drawing a deposit (DownPaymentsToDraw, net amount), collection (payment on real open invoices, E-TND-004 over-balance guard), customer-create (Business Partner), drawer-payment (account payment or journal per financial type), journal-over-short, journal-safe-drop; generic reverse cancels every resource newest first and reverses down payments with a credit memo (BaseType 203), persisting progress for resume.
  • Service Layer facts applied: payments carry DiscountPercent 0 (customer cash-discount terms otherwise reject same-day deposit payments), credit memos link only open invoices, negative SumApplied for the offsetting document, deposit payments take SAP's date, error -2038 (concurrent sessions) retried as E-PST-008, an unposted deposit makes the dependent sale wait (E-TND-003 transient) instead of failing.
  • Endpoints: POST /registers/{id}/customers, GET /registers/{id}/customers/{cardCode}/account (balance, terms, open invoices, open deposits), GET /registers/{id}/tickets/lookup, admin customer-requests list / retry; Operations shows non-ticket jobs (shift journals, drawer payments, customer requests) with tertiary documents.
  • Register: returns screen (lookup by number, barcode or ULID, local then server, per-line quantities, reasons, restock / damaged, exchange), tender screen refund mode and on-account tender with offline credit check (unsynced exposure counted, manager override), customer sheet (search, quick-create offline, account view, collect, take / use deposit), shift cash in/out with financial types, schema v4; EN/AR/FR strings.
  • Tests: server 93 (63 domain, 4 architecture, 7 API, 19 integration of which 7 live M4 flows on SBODEMOGB, all reversed), register 40 (M4 controller drills incl. E-TKT-009, exchange netting, credit check, outbox order; returns screen EN + RTL).
  • Exit drills passed live: pos/tool/m4_drill_test.dart (enrol, sync, offline quick-create + on-account + sale + return + deposit + sale drawing it + collection + cash out with financial type + Z with over/short, FIFO upload of 10 rows) and tools/dev/m3_drill_server.py post-all (every job succeeded first attempt, every posting reversed, queue clean).
  • Contracts: openapi.yaml (customers, account, ticket lookup, customer requests, snapshot kinds and return fields), errors.md (E-TKT-009, E-TND-001..004, E-CUS-001..004, E-CSH-001, E-PST-011..015, E-PST-008 widened).
  • Not in this build: store-credit tender (M7), portal page for customer requests (endpoints exist; Operations lists the jobs), Windows desktop build still blocked by Developer Mode on the build machine.
0.2.0
  • M3 (branch m3-checkout-posting) — retail checkout, tenders, receipts, shifts, offline outbox, SAP posting engine and Operations (spec docs/specs/M3_Checkout_Posting_Spec.md).
  • Server: ticket intake (POST /registers/{id}/tickets, idempotent by ULID, totals re-verified, hash chain verified against the register; faults park the ticket and never reject the upload), shift and drawer-movement intake, tickets / ticket_lines / ticket_tenders / ticket_events / shifts / drawer_movements / post_jobs with RLS (migration M3_CheckoutPosting).
  • Posting engine: PostingEngine + PostingWorker (claim with FOR UPDATE SKIP LOCKED, back-off 1/2/4/8 min, 5 attempts, stale-job reclaim), CashSaleProcess (A/R Invoice then Incoming Payment; the invoice DocEntry is persisted before the payment so a crash re-runs only step 2), ReverseProcess (cancel payment, then invoice), Service Layer error mapper E-PST-001..099, raw request / response / timing kept per job.
  • Operations: portal pages Operations (queue, summary cards, detail drawer with payloads and timing, retry / void / reverse / repost, run now) and Tickets (search, snapshot, events, jobs); "Provision SAP fields" creates U_CP_Ticket / U_CP_Register / U_CP_Operator / U_CP_Shift on OINV and ORCT when missing; currency rates endpoint (SBOBobService_GetCurrencyRate, cached per day).
  • Register: event-sourced ticket book (append-only events, rebuild on start, sequence + SHA-256 chain link + frozen snapshot + outbox row in one transaction), fixed-point Dec arithmetic mirroring the server's C# decimal (shared vectors, same chain link), checkout lane (smart item field, quick keys, F2/F4/F5/F8/F9/F12, @qty #price %discount wildcards), tender (cash quick buttons, foreign cash, standalone card), receipt composer (42-column text preview, ESC/POS with QR, network 9100 printer), shifts (open with float, cash in/out, safe drop, no-sale, X report, Z close with over/short), manager approvals with rank check, EN/AR/FR strings.
  • Tests: server 76 (domain totals/rounding/error mapping/job lifecycle, intake idempotency and parking, LIVE cash sale posted to SBODEMOGB with crash-between-steps recovery, tax-inclusive ticket, reversal verified in SAP), register 33 (state machine, power-loss rebuild, offline outbox drill with Idempotency-Key, shift Z, receipt golden, lane widget test incl. RTL).
  • Exit drills passed against the dev server and SBODEMOGB: network drop, power loss, FIFO upload, replay = alreadyStored, worker posting, Service Layer down (E-SAP-004 with back-off, retry succeeds), reversals (tools/dev/m3_drill_server.py, pos/tool/m3_drill_test.dart).
  • Contracts: openapi.yaml (tickets, shifts, drawer movements, currency rates, admin tickets / post-jobs / run-once / sap-udfs provision), errors.md (E-TKT-004..008, E-SHF-001..006, E-REQ-001, E-PST-001..105, E-CC-003, E-OUT-001).
  • Not in this build (documented gaps): Windows spooler printing and Arabic receipt raster (M7), card terminals through ITenderTerminal (M6), Windows desktop build still blocked by Developer Mode on the build machine.
0.1.0
  • M0 bootstrap: repository rules (CLAUDE.md), product plan v1.0, feature catalog v0.2, Phase 1 FRS, four research inventories.
  • Server solution: Domain (ULID, Money, ticket hash chain), Application, Infrastructure (EF Core / Npgsql context), Plugins.Abstractions (ITenderTerminal, IFiscalProvider, IPosExtension), Fiscal.None provider, Api host (Serilog, OpenAPI, /health/live, /health/ready, /api/v1/meta), Web (MudBlazor portal shell), Agent worker. 22 tests: domain, architecture layering, API.
  • Register app: Flutter skeleton with Riverpod, go_router, EN/AR/FR localization, theme tokens, splash / login / menu, widget tests incl. RTL pass and hit-target check.
  • Contracts: openapi.yaml, asyncapi.yaml, errors.md. CI workflow (server, register, contracts). NSIS installer skeletons. Version bump tool.
  • M2 (branch m2-master-data): SAP connection manager, live master data sync from Service Layer (10 entities, delta + full loads), versioned register pull, POS item attributes, quick-selection layouts with designer, configuration profiles with effective merge; register master data mirror and catalogue screen. Exit drill passed live against SBODEMOGB.
  • M1 (in progress on branch m1-identity-enrolment): tenancy with row-level security, licensing with trial, register enrolment with device certificates and device-assertion tokens, users / roles / rights with rank rules and templates, audit, portal (Blazor Server) and register enrolment + offline PIN login. Exit drill passed live.

تريد البناء التالي على نسختك التجريبية؟

اطلب عرضاً توضيحياً