The point-of-sale checkout flow: Checkout (the cart/session), Line (line items), and the billing records a processed checkout produces — Payment and Invoice. All classes live under zensile-sdk/company/member/checkout/. See Members for Member, whose convenience methods (member.checkouts(), .payments(), .invoices()) front this module.
company/member/checkout/checkout.ts. A POS checkout session for a member — the cart/order that Lines attach to, and that Invoice/Payment records are generated from once processed.
interface LazyCheckoutData {
id: string;
memberID?: string;
soldByMemberID?: string;
payingMemberID?: string;
companyID: string;
status: "pending" | "active" | "processing" | "resolved" | "canceled" | "expired";
amount: number;
subtotalAmount: number;
}
interface CheckoutData extends LazyCheckoutData {
stripeCheckoutID?: string;
stripePaymentIntentID?: string;
stripeSubscriptionID?: string;
resolvedAt?: Date;
canceledAt?: Date;
createdAt: Date;
updatedAt: Date;
}memberID/soldByMemberID/payingMemberIDare distinct roles: who the checkout is for, which staff member rang it up, and who's actually being charged (e.g. a group leader paying for another member) — the latter two are optional and conceptually fall back to memberID/ user when unset. amount/subtotalAmount are server-computedand reflect the checkout's current line items as of the last fetch.
interface CreateCheckoutData {
memberID?: string; // omit for a walk-in sale of non-recurring items — recurring items require a member
soldByMemberID?: string;
}Checkout.create(company: Company, checkouts: CreateCheckoutData[]): Promise<EventData<string[]>>
checkout.save(): Promise<SimpleResult<EventData>>
checkout.delete(): Promise<SimpleResult<EventData>> // delegates to Cancel() below — voids rather than hard-deletes
Checkout.query(company: Company, data: QueryRequest): Promise<SimpleResult<Checkout[]>>Endpoint: /company/{cID}/checkouts (/query for search). Detail route: /company/{cID}/checkouts/{id}.
fetch() eagerly resolves lines (via Line.query) and the related Members (user/soldByMember/payingMember) alongside the checkout itself, so those getters don't need a second round trip after a load.
type PaymentType = "cash" | "card" | "account" | "terminal";
interface ProcessCheckoutResult {
message: string;
checkoutID?: string;
stripePaymentIntentID?: string;
stripeSubscriptionID?: string;
amount?: number;
clientSecret?: string;
setupIntentID?: string;
requiresSetup?: boolean;
requiresConfirmation?: boolean;
type?: string;
requiresCollection?: boolean; // "terminal" only, when anything is owed
paymentIntentID?: string; // "terminal" only, when anything is owed
}
checkout.Process(paymentType: PaymentType, paymentMethodId?: string, promotionalCode?: string): Promise<SimpleResult<ProcessCheckoutResult>>
checkout.Cancel(): Promise<SimpleResult<EventData>> // voids a pending checkout without charging itpaymentType: "cash"/"account"/"terminal" are staff-only and rejected (400) for a checkout with a recurring-priced line; sent to the backend as processType (promotionalCode is sent as promotionCode)."cash"/"account" skip Stripe card charging entirely; "terminal" still charges a card, but via a staff-mediated physical reader — if anything is owed it creates a bare card-present PaymentIntent (requiresCollection: true, paymentIntentID, clientSecret) and the checkout stays "processing" until the Stripe webhook resolves or frees it once the reader collection completes; hand clientSecret to the client-side Stripe Terminal SDK along with a connection token from company.CreateTerminalConnectionToken(). Plain "card" uses paymentMethodId. Always check requiresConfirmation/requiresSetup on the result — Stripe may require client-side confirmation before the charge is final; clientSecret/setupIntentID are populated when it does. checkout.delete() is just Cancel()under the hood — deleting a checkout voids it, it's never hard-deleted.company.CreateTerminalConnectionToken(): Promise<SimpleResult<{ secret: string }>> (zensile-sdk/company/member/checkout/terminal.ts) mints the short-lived Stripe Terminal connection token the client-side Terminal SDK needs to discover and connect to a physical card reader (Bluetooth Chipper/WisePad, or Tap to Pay) — the reader itself never talks to this API. Staff-only, same bar as processType: "terminal" above.document-type Feature, the first Process() call only moves the checkout to "processing" and generates the buyer's MemberDocument(s) as a side effect — the sale is not finalized yet. Query for status: "unsigned" documents sourced from this checkout (checkout.GetDocuments(), filtered to ones whose template has requiresSignature), have the buyer Sign() each one, then call checkout.Process() again with the same arguments to complete the sale. See checkouts/[checkoutID]/process/page.tsx's finalizeAfterSigning for the reference implementation.checkout.isPending: boolean // status === "active"
checkout.isProcessed: boolean // status === "resolved"
checkout.isCancelled: boolean // status === "canceled"Each queries a related resource type filtered by this checkout's id:
checkout.GetInvoices({ limit?, offset?, lazy? }): Promise<SimpleResult<Invoice[]>> // filtered by checkoutID
checkout.GetPayments({ limit?, offset?, lazy? }): Promise<SimpleResult<Payment[]>> // filtered by checkoutID
checkout.GetSubscriptions({ limit?, offset?, lazy? }): Promise<SimpleResult<Subscription[]>> // filtered by checkoutID
checkout.GetCredits({ limit?, offset?, lazy? }): Promise<SimpleResult<Credit[]>> // filtered by sourceCheckoutID
checkout.GetDiscounts({ limit?, offset?, lazy? }): Promise<SimpleResult<Discount[]>> // filtered by checkoutID
checkout.GetDocuments({ limit?, offset?, lazy? }): Promise<SimpleResult<MemberDocument[]>> // filtered by sourceCheckoutIDEach defaults to limit: 100, offset: 0, lazy: true.
checkout.addItem(item: Product | Service | Booking | Package, quantity: number): Promise<Line[]>Adds a sellable item to the cart, or increments its quantity if a matching line already exists. Re-queries _lines from the server first (Line.query(this.company, this, true)) before scanning for an existing match, to avoid acting on a stale local cache. Dispatches which id field to set (productID/serviceID/bookingID/packageID) via instanceof checks against Product/Service/Booking/Package. If no matching line is found, creates a new one via Line.create(), then re-queries lines again before returning.
checkoutData.subtotalAmountin reactive UI — it's server-computed and goes stale until the next fetch. Derive it locally instead from each line's quantity * product.price (e.g. via useMany(lines)in the app's React layer).company/member/checkout/line.ts. A line in a checkout's cart. Exactly one of productID/serviceID/bookingID/packageIDis normally set to say what's being sold; userIDoptionally attributes the line to someone other than the checkout's own member.
interface LazyLineData {
id: string;
productID?: string | null;
serviceID?: string | null;
bookingID?: string | null;
packageID?: string | null;
checkoutID: string;
userID?: string | null;
quantity: number;
amount?: number;
discount?: number;
}
interface LineData extends LazyLineData {
product?: Product; // NOT populated by fetch()/query() — use the product getter instead of reading this field
createdAt: Date;
updatedAt: Date;
}
type CreateLineData = Pick<LazyLineData, "quantity"> & {
productID?: string;
checkoutID: string;
serviceID?: string;
bookingID?: string;
packageID?: string;
userID?: string;
price?: number; // optionally overrides the target's default price (e.g. group/discount pricing) for this line only
};Line.create(company: Company, lines: CreateLineData[]): Promise<EventData<string[]>>
line.save(): Promise<SimpleResult<EventData>> // PATCH, e.g. quantity
line.delete(): Promise<SimpleResult<EventData>>
Line.query(company: Company, checkout: Checkout, lazy?: boolean): Promise<SimpleResult<Line[]>> // takes a Checkout directly, not a general QueryRequest filterEndpoint: /company/{cID}/checkouts/lines.
line.IncrementQuantity(amount: number): Promise<void>Adjusts quantity by a delta (positive or negative). Computes newQty = quantity + amount; if newQty <= 0, calls delete() and returns instead of saving. Otherwise updates local data via setData({ ...this._data, quantity: newQty }) (so subscribers are notified before the network call resolves) and then calls save().
At most one of the four resolves to a value, matching LazyLineData's "exactly one target" shape — each cached in a private field:
line.checkout: Checkout | undefined
line.product: Product | undefined
line.service: Service | undefined
line.booking: Booking | undefined
line.package: Package | undefinedcompany/member/checkout/payment.ts. A payment/charge attempt against a checkout — maps to the backend's /payments endpoints, distinct from Invoice (the billing document it may settle, below). Read-only except for refunds.
type PaymentStatus = "succeeded" | "failed" | "refunded" | "partially_refunded";
interface LazyPaymentData {
id: string;
companyID: string;
checkoutID: string;
invoiceID?: string | null; // optional — not every payment (e.g. a one-off card charge) is tied to a Stripe invoice
status: PaymentStatus;
currency: string;
amount: string;
amountRefunded: string;
createdAt: Date;
updatedAt: Date;
}
interface PaymentData extends LazyPaymentData {
stripePaymentIntentID?: string | null;
stripeChargeID?: string | null;
stripeCustomerID?: string | null;
paymentMethodType?: string | null;
cardBrand?: string | null;
cardLast4?: string | null;
receiptUrl?: string | null;
failureCode?: string | null;
failureMessage?: string | null;
paidAt?: string | null;
refundedAt?: string | null;
}payment.save(): Promise<SimpleResult<EventData>> // always rejected — { status: 400, message: "Payments cannot be updated directly." }
payment.delete(): Promise<SimpleResult<EventData>> // always rejected — { status: 400, message: "Payments cannot be deleted directly." }
payment.refund(amount?: number, reason?: string): Promise<SimpleResult<EventData>> // omit amount to refund in full
Payment.query(company: Company, data: QueryRequest): Promise<SimpleResult<Payment[]>>Endpoint: /company/{cID}/payments (/query for search, /refund for refund()).
payment.checkout: Checkout | undefined
payment.invoice: Invoice | undefined
payment.isSucceeded: boolean
payment.isFailed: boolean
payment.isRefunded: boolean
payment.isPartiallyRefunded: booleanMember.payments(opts)is the parent → children convenience wrapper (resolves the member's checkout ids first, since payments has no memberID column). checkout.GetPayments(opts) is the checkout-scoped equivalent.
company/member/checkout/invoice.ts. A Stripe billing document (invoice) generated for a checkout — distinct from Payment (the actual charge attempt against it, above). Read-only: save/delete are stubs since invoices are backend/Stripe-managed.
type InvoiceStatus = "draft" | "open" | "paid" | "uncollectible" | "void" | "refunded";
interface LazyInvoiceData {
id: string;
checkoutID: string;
companyID: string;
status: InvoiceStatus;
currency: string;
amountDue: string; // amount fields are strings, mirroring Stripe's own decimal-string representation
amountPaid: string;
amountRemaining: string;
invoiceNumber?: string;
createdAt: Date;
updatedAt: Date;
}
interface InvoiceData extends LazyInvoiceData {
stripeInvoiceID?: string;
stripePaymentIntentID?: string | null;
stripeChargeID?: string | null;
stripeCustomerID?: string;
stripeSubscriptionID?: string;
description?: string | null;
invoicePdfUrl?: string;
hostedInvoiceUrl?: string;
billingReason?: string;
periodStart?: string;
periodEnd?: string;
dueDate?: string | null;
paidAt?: string | null;
}invoice.save(): Promise<SimpleResult<EventData>> // always rejected — { status: 400, message: "Invoices cannot be updated directly." }
invoice.delete(): Promise<SimpleResult<EventData>> // always rejected — { status: 400, message: "Invoices cannot be deleted directly." }
invoice.Refund(): Promise<SimpleResult<EventData>> // refunds this invoice in full via Stripe
Invoice.query(company: Company, data: QueryRequest): Promise<SimpleResult<Invoice[]>>Endpoint: /company/{cID}/invoices (/query for search, /refund for Refund()).
invoice.checkout: Checkout | undefined is the only related-entity getter. Member.invoices(opts)is the parent → children convenience wrapper (resolves the member's checkout ids first, since invoices has no memberID column). checkout.GetInvoices(opts) is the checkout-scoped equivalent. A Payment optionally links to the Invoice it settles via invoiceID; an Invoice does not link back to a specific Payment.