Product store — Behavior
Store readiness
ProductStoreAccessService.storeStatus(orgId):
| Status | When |
|---|---|
ready | Active provider config with status = 'active' and provider in STORE_PROVIDERS (cardcom). |
no_provider | No active config, or the terminal is not active (for example pending_kyc). |
unsupported_provider | Any other provider. |
Creating a product, restoring one, a desk sale, and a checkout all require ready (409 product_store_unavailable). Reading the catalog and the sales list never does.
Card checkout (POST /public/products/:token/checkout)
- Token → product → org. Unknown token, or an org without the
product_storetier feature: 404. - Archived product: 409
product_unavailable. - Validate: name 2–120 characters, phone through
normalizePhone(E.164), quantity 1–20. - Member match: session first (
bySession), then phone (byPhone, exactly one active member). - Write the pending
payment_transactionsrow (metadata.source = 'shop_order',orderId= sale id), then theproduct_salesrow. - Mint the Cardcom page:
chargeOnly,Documentwith the buyer name and a line ofquantity × unit price, cardholder phone pre-filled, return URL/pay/result?txn=…&receipt=…&status=…. - Page failure: transaction
failed, 503payment_page_failed. - Store the page id on the transaction (
provider_transaction_id) and on the sale (process_id).
Rate limit: 10 checkouts per minute per client IP (@Throttle).
Settlement (WebhookProcessingService.settleProductSale)
Runs on a verified payment.completed, keyed on lowProfileId, before the generic deal-id lookup.
- Settles from
pending,failedorcancelled— a late payment on a swept checkout still lands. - Goes through
PaymentTransactionService.updateStatus, so the sale is on the charge audit trail. - Records the Cardcom document (
DocumentInfofromGetLpResult) on the transaction. - Emits
product_sale.paid;ProductReceiptServicesends the receipt. - Unlike payment-link settlement, a failure here propagates: the webhook fails and Cardcom redelivers.
Abandoned checkouts
ProductSalesService.closeAbandonedCheckouts (hourly, :41) moves link sales still pending after 24 hours to cancelled.
Desk sale (POST /organizations/:orgId/product-sales)
provider = 'manual', status = 'completed', the chosen method (cash, bit, bank_transfer, other). Idempotent on the client key (product-desk:<orgId>:<key>). The tax document is issued through ManualDocumentService (tax_invoice_receipt); a failure lands in the owed-documents queue. A receipt is sent when the buyer has a phone (typed, or the linked member’s).
Refunds
Through the existing POST /organizations/:orgId/payments/:txnId/refund. Card sales: Cardcom refund plus credit document. Desk sales: the cash credit-note dialog. The sale’s status follows its transaction (refund_pending/refunded → refunded).
Receipt sending (ProductReceiptService.send)
- Phone: the buyer phone, else the linked member’s phone. None:
receipt_error = 'no_phone'. - Claim:
receipt_sent_atset where null (skipped for a staff resend). - WhatsApp, when
WhatsAppSenderService.isConfigured(org)and the template is approved. A missing template is provisioned for next time (ensureSystemTemplate) and this sale falls through to SMS. A member’s WhatsApp billing preference applies. - SMS, when
OrganizationSmsSettingsService.senderFor(org)returns a sender.unknownresults are recorded, never retried. - Neither:
receipt_error = 'no_channel'.
Receipt link (GET /public/receipts/:token)
unpaid until the transaction is completed or refunded; then ready with the document URL, or pending while Taikan has no URL. With a document number but no URL, the URL is fetched from Cardcom (getDocument) and cached on the row.
Reports
- Every revenue total already counted store money (it is
payment_transactionslike any other). RevenueReport.bySourceand therevenue-sourcesreport view split net receipts intostore(a sale’s charge and refunds pointing at it viametadata.refundedTransactionId) andother.- The payments transaction list carries
productNameandbuyerNamefor store rows; search matches them.