Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesProduct StoreProduct store — Behavior

Product store — Behavior

Store readiness

ProductStoreAccessService.storeStatus(orgId):

StatusWhen
readyActive provider config with status = 'active' and provider in STORE_PROVIDERS (cardcom).
no_providerNo active config, or the terminal is not active (for example pending_kyc).
unsupported_providerAny 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)

  1. Token → product → org. Unknown token, or an org without the product_store tier feature: 404.
  2. Archived product: 409 product_unavailable.
  3. Validate: name 2–120 characters, phone through normalizePhone (E.164), quantity 1–20.
  4. Member match: session first (bySession), then phone (byPhone, exactly one active member).
  5. Write the pending payment_transactions row (metadata.source = 'shop_order', orderId = sale id), then the product_sales row.
  6. Mint the Cardcom page: chargeOnly, Document with the buyer name and a line of quantity × unit price, cardholder phone pre-filled, return URL /pay/result?txn=…&receipt=…&status=….
  7. Page failure: transaction failed, 503 payment_page_failed.
  8. 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, failed or cancelled — 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 (DocumentInfo from GetLpResult) on the transaction.
  • Emits product_sale.paid; ProductReceiptService sends 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)

  1. Phone: the buyer phone, else the linked member’s phone. None: receipt_error = 'no_phone'.
  2. Claim: receipt_sent_at set where null (skipped for a staff resend).
  3. 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.
  4. SMS, when OrganizationSmsSettingsService.senderFor(org) returns a sender. unknown results are recorded, never retried.
  5. Neither: receipt_error = 'no_channel'.

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_transactions like any other).
  • RevenueReport.bySource and the revenue-sources report view split net receipts into store (a sale’s charge and refunds pointing at it via metadata.refundedTransactionId) and other.
  • The payments transaction list carries productName and buyerName for store rows; search matches them.