SDK Reference

Checkout (POS)

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.

Checkout

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.

ts
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.

ts
interface CreateCheckoutData {
  memberID?: string;       // omit for a walk-in sale of non-recurring items — recurring items require a member
  soldByMemberID?: string;
}

CRUD

ts
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.

Process / Cancel

ts
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 it
Note:paymentType: "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.
Note: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-required checkouts need a second Process() call:If a purchased product carries a 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.

Status helpers

ts
checkout.isPending: boolean      // status === "active"
checkout.isProcessed: boolean    // status === "resolved"
checkout.isCancelled: boolean    // status === "canceled"

Parent → children convenience methods

Each queries a related resource type filtered by this checkout's id:

ts
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 sourceCheckoutID

Each defaults to limit: 100, offset: 0, lazy: true.

addItem

ts
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.

Gotcha (consumer-facing, enforced by convention, not by this SDK):Do not compute a running subtotal from 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).

Line

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.

ts
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
};

CRUD

ts
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 filter

Endpoint: /company/{cID}/checkouts/lines.

IncrementQuantity

ts
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().

Getters

At most one of the four resolves to a value, matching LazyLineData's "exactly one target" shape — each cached in a private field:

ts
line.checkout: Checkout | undefined
line.product: Product | undefined
line.service: Service | undefined
line.booking: Booking | undefined
line.package: Package | undefined

Payment

company/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.

ts
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;
}
ts
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()).

Getters / status helpers

ts
payment.checkout: Checkout | undefined
payment.invoice: Invoice | undefined
payment.isSucceeded: boolean
payment.isFailed: boolean
payment.isRefunded: boolean
payment.isPartiallyRefunded: boolean

Member.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.

Invoice

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.

ts
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;
}
ts
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.