@aglyn/plugins-commerce 1.0.0-beta.278 → 1.0.0-beta.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -14
- package/src/lib/components/commerce-console-page.js +33 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
- package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.js +5 -0
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
- package/src/lib/components/console/pos-hardware-card.component.js +456 -0
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +6 -3
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-zones.d.ts +37 -3
- package/src/lib/components/console/pos-zones.js +11 -1
- package/src/lib/components/console/pos-zones.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
- package/src/lib/components/console/product-check-in-fields.component.js +172 -0
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +48 -9
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
- package/src/lib/components/console/product-register-fields.component.js +39 -1
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +21 -6
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +239 -70
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/reservation-widget.js +16 -0
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/constants/operator-alerts.d.ts +6 -0
- package/src/lib/constants/operator-alerts.js +20 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +7 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +34 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/model/commerce-check-in-product.d.ts +37 -0
- package/src/lib/model/commerce-check-in-product.js +49 -0
- package/src/lib/model/commerce-check-in-product.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +187 -0
- package/src/lib/model/commerce-check-in.js +468 -0
- package/src/lib/model/commerce-check-in.js.map +1 -0
- package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
- package/src/lib/model/commerce-pos-day-report.js +572 -0
- package/src/lib/model/commerce-pos-day-report.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +65 -3
- package/src/lib/model/commerce-pos-offline.js +104 -7
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.d.ts +3 -0
- package/src/lib/model/commerce-pos-ops.js +1 -0
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +43 -3
- package/src/lib/model/commerce-pos.js +73 -5
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +73 -7
- package/src/lib/model/commerce-reservations.js +90 -10
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +40 -0
- package/src/lib/model/commerce.js +22 -4
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +2 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-hardware.d.ts +288 -0
- package/src/lib/model/pos-hardware.js +499 -0
- package/src/lib/model/pos-hardware.js.map +1 -0
- package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
- package/src/lib/model/pos-offline-card-limits.js +28 -0
- package/src/lib/model/pos-offline-card-limits.js.map +1 -0
- package/src/lib/model/pos-reader-splash.d.ts +114 -0
- package/src/lib/model/pos-reader-splash.js +141 -0
- package/src/lib/model/pos-reader-splash.js.map +1 -0
- package/src/lib/model/product-write.d.ts +6 -0
- package/src/lib/plugin-config.d.ts +10 -0
- package/src/lib/plugin-config.js +40 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/cart-checkout.js +3 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +12 -1
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.js +49 -12
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +1 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +7 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +8 -1
- package/src/lib/server/person-eraser.js +23 -1
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-check-in.d.ts +49 -0
- package/src/lib/server/pos-check-in.js +652 -0
- package/src/lib/server/pos-check-in.js.map +1 -0
- package/src/lib/server/pos-day-report.d.ts +46 -0
- package/src/lib/server/pos-day-report.js +299 -0
- package/src/lib/server/pos-day-report.js.map +1 -0
- package/src/lib/server/pos-hardware-admin.d.ts +50 -0
- package/src/lib/server/pos-hardware-admin.js +366 -0
- package/src/lib/server/pos-hardware-admin.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +27 -0
- package/src/lib/server/pos-hardware-notify.js +142 -0
- package/src/lib/server/pos-hardware-notify.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
- package/src/lib/server/pos-hardware-user-eraser.js +67 -0
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
- package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
- package/src/lib/server/pos-hardware-webhook.js +176 -0
- package/src/lib/server/pos-hardware-webhook.js.map +1 -0
- package/src/lib/server/pos-hardware.d.ts +58 -0
- package/src/lib/server/pos-hardware.js +234 -0
- package/src/lib/server/pos-hardware.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
- package/src/lib/server/pos-offline-card-reconcile.js +58 -0
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +32 -0
- package/src/lib/server/pos-offline-sync.js +191 -15
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-routes.d.ts +4 -2
- package/src/lib/server/pos-ops-routes.js +14 -2
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order.js +39 -51
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +8 -4
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
- package/src/lib/server/pos-reader-splash-admin.js +295 -0
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
- package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
- package/src/lib/server/pos-reader-splash-render.js +138 -0
- package/src/lib/server/pos-reader-splash-render.js.map +1 -0
- package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
- package/src/lib/server/pos-reader-splash-server.js +32 -0
- package/src/lib/server/pos-reader-splash-server.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +105 -0
- package/src/lib/server/pos-reader-splash.js +548 -0
- package/src/lib/server/pos-reader-splash.js.map +1 -0
- package/src/lib/server/pos-sale.d.ts +2 -1
- package/src/lib/server/pos-sale.js +3 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +67 -4
- package/src/lib/server/pos-terminal.js +239 -32
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/process-restock.js +4 -0
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-card.js +2 -2
- package/src/lib/server/product-card.js.map +1 -1
- package/src/lib/server/product-catalog.d.ts +2 -0
- package/src/lib/server/product-catalog.js +11 -4
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product.js +3 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/related.js +8 -1
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +35 -0
- package/src/lib/server/reservation-availability-query.js +34 -0
- package/src/lib/server/reservation-availability-query.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +21 -0
- package/src/lib/server/reservation-availability.js +64 -10
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-walk-in.d.ts +28 -0
- package/src/lib/server/reservation-walk-in.js +166 -0
- package/src/lib/server/reservation-walk-in.js.map +1 -0
- package/src/lib/server/reserve.js +13 -10
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/reviews.js +3 -1
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +5 -2
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +72 -0
- package/src/lib/testing/fake-firestore.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-reader-splash.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport { parseMediaCdnScope, type MediaCdnScope } from '@aglyn/aglyn/app-utils/media-cdn-scope'\nimport {\n formatMediaRef,\n isMediaCdnPath,\n MEDIA_CDN_ROUTE,\n mediaRefFromStorageUrl,\n parseMediaRef,\n resolveMediaSrc,\n} from '@aglyn/aglyn/app-utils/media-ref'\nimport { checkEntitlement, resolveBrandingProfile } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport {\n isPosReaderSplashChoice,\n POS_READER_SPLASH_DEVICE_TYPES,\n POS_READER_SPLASH_FAMILIES,\n POS_READER_SPLASH_SOURCE_MAX_BYTES,\n POS_READER_SPLASH_SOURCE_TYPES,\n posReaderSplashArtworkProblem,\n posReaderSplashEffectiveChoice,\n type PosReaderSplashChoice,\n type PosReaderSplashFamily,\n type PosReaderSplashFamilyId,\n type PosReaderSplashSetting,\n type PosReaderSplashStatus,\n} from '../model/pos-reader-splash'\nimport { authorizePosStaff, posRequestBody } from './pos-auth'\nimport {\n PosReaderSplashRenderError,\n posReaderSplashImageFacts,\n renderPosReaderSplash,\n type RenderedPosReaderSplash,\n} from './pos-reader-splash-render'\nimport { POS_STRIPE_VERSION, posTerminalAvailable } from './pos-stripe'\nimport { savePosReaderConfiguration } from './pos-terminal'\n\n/*==========================================\n * A STORE'S CARD READERS SHOW ITS START-UP SCREEN (AGL-4048).\n *\n * ## Where it lives\n *\n * Readers and Locations are the PLATFORM's (the destination-charge model,\n * `pos-terminal.ts`), one Location per site. The screen is set on that\n * site's one Terminal Configuration, the one reader tipping already uses,\n * per device type (`stripe_s700`, `stripe_s710`, `bbpos_wisepos_e`), from a\n * File uploaded to Stripe with purpose `terminal_reader_splashscreen`.\n *\n * The platform's account-default configuration is NEVER given a screen. A\n * Location without an override inherits it, so an account default would put\n * the platform's brand on a white-label store's readers, which is the one\n * thing the white-label rule forbids. Staff's default screen is applied per\n * Location instead, to the stores that use it.\n *\n * ## What is applied\n *\n * `hosts/{hostId}/terminal/config.splash` holds the store's choice\n * (`model/pos-reader-splash.ts`); the screen applied is described by a KEY\n * built from cheap metadata only: the choice, the platform default's\n * version, the logo's address and its asset's content digest, the picked\n * image's. `splashAppliedKey` records the last key that reached Stripe, so\n * applying again with nothing changed makes no Stripe call at all, and a\n * changed logo, a new staff default or a new choice makes exactly one round\n * of uploads. Every upload and the first configuration write are keyed by\n * content, so a retried apply never makes a second file or configuration.\n *\n * ## When it is applied\n *\n * - when the store's Location is created (its first reader);\n * - when the POS devices card saves a choice, or opens and finds the key\n * stale (a logo changed since);\n * - when staff change the default, for every store with a Location\n * (`pos-reader-splash-admin.ts`).\n *\n * Never on the payment path: drawing and uploading takes a second or two,\n * and a reader with an older screen still takes the payment.\n *\n * ## Test mode first\n *\n * Gated by `posTerminalAvailable()`: always in test mode, in live mode only\n * once `STRIPE_TERMINAL_LIVE_ENABLED` is on, like every reader control.\n *=========================================*/\n\n/** Raised when the drawing or the Location changes how keys are built. */\nconst RENDER_VERSION = 'v1'\n\n/** `platformSettings/posReaderSplash`: staff's default screen. */\nexport const POS_READER_SPLASH_SETTINGS_ID = 'posReaderSplash'\n\n/** One family's default screen, as staff uploaded it. */\nexport interface PlatformSplashImage {\n storagePath: string\n sha256: string\n contentType: 'image/png' | 'image/jpeg'\n bytes: number\n uploadedAtMs: number\n uploadedBy?: string\n}\n\nexport interface PlatformSplashDoc {\n families?: Partial<Record<PosReaderSplashFamilyId, PlatformSplashImage>>\n updatedAtMs?: number\n}\n\nconst firestore = () => firebaseAdmin.app().firestore()\n\nfunction terminalConfigRef(hostId: string) {\n return firestore().collection('hosts').doc(hostId).collection('terminal').doc('config')\n}\n\nexport function platformSplashRef() {\n return firestore().collection('platformSettings').doc(POS_READER_SPLASH_SETTINGS_ID)\n}\n\nexport function posReaderSplashBucket() {\n return firebaseAdmin\n .app()\n .storage()\n .bucket(process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || undefined)\n}\n\nexport async function readPlatformSplash(): Promise<PlatformSplashDoc> {\n const snapshot = await platformSplashRef().get()\n return (snapshot.exists ? snapshot.data() : {}) as PlatformSplashDoc\n}\n\n/** The default is usable once the S700/S710 screen is uploaded; the smaller one is drawn from it if absent. */\nexport function platformSplashReady(doc: PlatformSplashDoc): boolean {\n return Boolean(doc.families?.['portrait-large']?.storagePath)\n}\n\nfunction platformSplashVersion(doc: PlatformSplashDoc): string {\n return POS_READER_SPLASH_FAMILIES.map((family) => doc.families?.[family.id]?.sha256 ?? '-').join('.')\n}\n\nconst digest = (value: string | Buffer) => createHash('sha256').update(value).digest('hex')\n\nconst cleanString = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/*==========================================\n * READING A SOURCE IMAGE.\n *\n * A media value is read from STORAGE, never fetched, when it names this\n * site's library or its org's: the document must be live, not private,\n * visible to this site, its scope not locked and the asset not quarantined,\n * and the object path inside the scope's media prefix (the rules the media\n * CDN serves by). An https address outside the libraries is fetched only from\n * a public address, pinned against DNS rebinding, with no redirects, a\n * timeout and a size cap.\n *=========================================*/\n\ninterface LocatedAsset {\n kind: 'asset'\n scope: MediaCdnScope\n scopeSegment: string\n mediaId: string\n doc: Record<string, unknown>\n /** What changes when the asset's bytes do. */\n version: string\n}\n\ninterface LocatedUrl {\n kind: 'url'\n url: string\n version: string\n}\n\ntype LocatedSource = LocatedAsset | LocatedUrl\n\nasync function locateMediaAsset(\n scopeSegment: string,\n mediaId: string,\n hostId: string,\n orgId: string,\n): Promise<LocatedAsset | null> {\n const scope = parseMediaCdnScope(scopeSegment)\n if (!scope || !/^[A-Za-z0-9_-]{1,64}$/.test(mediaId)) return null\n const owned = scope.isOrg ? scope.scopeId === orgId : scope.scopeId === hostId\n if (!owned) return null\n const snapshot = await firestore()\n .collection(scope.isOrg ? 'orgs' : 'hosts')\n .doc(scope.scopeId)\n .collection('media')\n .doc(mediaId)\n .get()\n const doc = (snapshot.exists ? snapshot.data() : null) as Record<string, unknown> | null\n if (!doc || doc['deletedAt'] || doc['private']) return null\n if (scope.isOrg && !visibleToHost(doc['visibleTo'] as string[] | undefined, hostId)) return null\n if (!/^image\\//.test(String(doc['contentType'] ?? ''))) return null\n if (typeof doc['sizeBytes'] === 'number' && doc['sizeBytes'] > POS_READER_SPLASH_SOURCE_MAX_BYTES) return null\n const version =\n cleanString(doc['contentSha256']) ||\n cleanString(doc['contentHash']) ||\n String(doc['updatedAtMs'] ?? doc['sizeBytes'] ?? '')\n return { kind: 'asset', scope, scopeSegment, mediaId, doc, version }\n}\n\n/** Where a stored logo or image value lives, or null when it cannot be read. */\nasync function locateSource(value: string, hostId: string, orgId: string): Promise<LocatedSource | null> {\n const stored = parseMediaRef(value) ? value : mediaRefFromStorageUrl(value)\n const ref = stored ? parseMediaRef(stored) : null\n if (ref) return locateMediaAsset(ref.scope, ref.mediaId, hostId, orgId)\n if (isMediaCdnPath(value)) {\n const rest = value.slice(`${MEDIA_CDN_ROUTE}/`.length).split('?')[0]\n const slash = rest.indexOf('/')\n if (slash <= 0) return null\n return locateMediaAsset(rest.slice(0, slash), rest.slice(slash + 1), hostId, orgId)\n }\n // `isMediaCdnPath` narrows a string it refuses to `never`; the value is still the string.\n const address: string = value as string\n if (/^https:\\/\\//.test(address) && address.length <= 2000) return { kind: 'url', url: address, version: address }\n return null\n}\n\nasync function readAssetBytes(asset: LocatedAsset): Promise<Buffer | null> {\n const { mediaCdnServeBlock, mediaStoragePathInScope } = await import('./pos-reader-splash-server')\n const blocked = await mediaCdnServeBlock(asset.scope, {\n contentSha256: typeof asset.doc['contentSha256'] === 'string' ? (asset.doc['contentSha256'] as string) : undefined,\n contentHash: typeof asset.doc['contentHash'] === 'string' ? (asset.doc['contentHash'] as string) : undefined,\n scopeSegment: asset.scopeSegment,\n mediaId: asset.mediaId,\n })\n if (blocked) return null\n const objectPath = mediaStoragePathInScope({\n storagePath: asset.doc['storagePath'],\n base: `${asset.scope.isOrg ? 'orgs' : 'hosts'}/${asset.scope.scopeId}`,\n mediaId: asset.mediaId,\n })\n const [buffer] = (await posReaderSplashBucket().file(objectPath).download()) as [Buffer]\n return buffer.length > POS_READER_SPLASH_SOURCE_MAX_BYTES ? null : buffer\n}\n\nconst FETCH_TIMEOUT_MS = 8000\n\nasync function fetchPublicImage(url: string): Promise<Buffer | null> {\n const { createPinnedDispatcher, resolvePublicIp } = await import('./pos-reader-splash-server')\n const pinnedIp = await resolvePublicIp(new URL(url).hostname)\n if (!pinnedIp) return null\n const dispatcher = createPinnedDispatcher(pinnedIp)\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)\n try {\n const init = { redirect: 'error' as const, signal: controller.signal, dispatcher }\n const response = await fetch(url, init as RequestInit)\n if (!response.ok) return null\n if (!/^image\\//.test(response.headers.get('content-type') ?? '')) return null\n const declared = Number(response.headers.get('content-length') ?? 0)\n if (declared > POS_READER_SPLASH_SOURCE_MAX_BYTES) return null\n const buffer = Buffer.from(await response.arrayBuffer())\n return buffer.length > POS_READER_SPLASH_SOURCE_MAX_BYTES ? null : buffer\n } catch {\n return null\n } finally {\n clearTimeout(timeout)\n ;(dispatcher as { close?: () => Promise<void> }).close?.().catch(() => undefined)\n }\n}\n\nasync function readSourceBytes(source: LocatedSource): Promise<Buffer | null> {\n return source.kind === 'asset' ? readAssetBytes(source) : fetchPublicImage(source.url)\n}\n\n/*==========================================\n * WHAT A STORE SHOWS.\n *=========================================*/\n\n/**\n * The workspace's own logo: a white-label workspace's brand logo first,\n * then the logo on its organization profile. Never the platform's: the\n * resolved brand profile falls back to the platform's own logo for a\n * workspace without a brand, so its `logoUrl` is not read here.\n */\nexport function workspaceLogoValue(org: Record<string, any> | null, whiteLabel: boolean): string | null {\n const brand = whiteLabel ? cleanString(org?.['brandingProfile']?.['logoUrl']) : ''\n return brand || cleanString(org?.['logoUrl']) || null\n}\n\ntype SplashSource =\n | { mode: 'platform'; doc: PlatformSplashDoc }\n | { mode: 'logo' | 'artwork'; located: LocatedSource }\n\nexport interface PosReaderSplashPlan {\n choice: PosReaderSplashChoice | null\n effectiveChoice: PosReaderSplashChoice\n whiteLabel: boolean\n productName: string\n platformReady: boolean\n logoValue: string | null\n image: { mediaScope: string; mediaId: string } | null\n /** `none` when the readers show Stripe's standard screen. */\n key: string\n problem: string | null\n source: SplashSource | null\n}\n\nconst NO_LOGO = 'Add a logo to your organization profile to show it on your card readers.'\nconst NO_IMAGE = 'The image picked for your card readers is no longer in your media library. Choose another.'\n\nexport async function planPosReaderSplash(\n hostId: string,\n org: Record<string, any> | null,\n config: Record<string, any> | undefined,\n platform?: PlatformSplashDoc,\n): Promise<PosReaderSplashPlan> {\n const orgId = String(org?.['id'] ?? '')\n const whiteLabel = checkEntitlement(org as any, 'whiteLabel')\n const productName = resolveBrandingProfile(org as any).productName\n const setting = (config?.['splash'] ?? null) as PosReaderSplashSetting | null\n const choice = isPosReaderSplashChoice(setting?.choice) ? setting.choice : null\n const effectiveChoice = posReaderSplashEffectiveChoice(choice, whiteLabel)\n const doc = platform ?? (await readPlatformSplash())\n const logoValue = workspaceLogoValue(org, whiteLabel)\n const image =\n setting?.mediaScope && setting?.mediaId ? { mediaScope: setting.mediaScope, mediaId: setting.mediaId } : null\n const base = {\n choice,\n effectiveChoice,\n whiteLabel,\n productName,\n platformReady: platformSplashReady(doc),\n logoValue,\n image,\n }\n if (effectiveChoice === 'platform') {\n // The white-label rule is in `posReaderSplashEffectiveChoice`; this is\n // the guard behind it, because this branch is the platform's brand.\n if (whiteLabel || !platformSplashReady(doc)) return { ...base, key: 'none', problem: null, source: null }\n return {\n ...base,\n key: `platform:${RENDER_VERSION}:${platformSplashVersion(doc)}`,\n problem: null,\n source: { mode: 'platform', doc },\n }\n }\n if (effectiveChoice === 'logo') {\n const located = logoValue ? await locateSource(logoValue, hostId, orgId) : null\n if (!located) return { ...base, key: 'none', problem: NO_LOGO, source: null }\n return {\n ...base,\n key: `logo:${RENDER_VERSION}:${digest(`${logoValue}|${located.version}`).slice(0, 24)}`,\n problem: null,\n source: { mode: 'logo', located },\n }\n }\n const located = image ? await locateMediaAsset(image.mediaScope, image.mediaId, hostId, orgId) : null\n if (!located) return { ...base, key: 'none', problem: NO_IMAGE, source: null }\n return {\n ...base,\n key: `image:${RENDER_VERSION}:${digest(`${image?.mediaScope}/${image?.mediaId}|${located.version}`).slice(0, 24)}`,\n problem: null,\n source: { mode: 'artwork', located },\n }\n}\n\nasync function loadSharpFactory(): Promise<any> {\n const { loadSharp } = await import('./pos-reader-splash-server')\n return loadSharp()\n}\n\n/** Each family's screen for the plan, ready for Stripe. */\nasync function drawScreens(\n source: SplashSource,\n): Promise<Record<PosReaderSplashFamilyId, RenderedPosReaderSplash>> {\n const sharp = await loadSharpFactory()\n const screens = {} as Record<PosReaderSplashFamilyId, RenderedPosReaderSplash>\n if (source.mode === 'platform') {\n const bucket = posReaderSplashBucket()\n const read = async (image: PlatformSplashImage) =>\n ((await bucket.file(image.storagePath).download()) as [Buffer])[0]\n const large = source.doc.families?.['portrait-large'] as PlatformSplashImage\n const largeBody = await read(large)\n for (const family of POS_READER_SPLASH_FAMILIES) {\n const own = source.doc.families?.[family.id]\n // Staff's own screen for the family is sent as uploaded: it was checked\n // against Stripe's rules for exactly this family then. A family staff\n // left empty is drawn from the S700/S710 screen, the same 9:16.\n screens[family.id] = own?.storagePath\n ? { body: own === large ? largeBody : await read(own), contentType: own.contentType }\n : await renderPosReaderSplash({ source: largeBody, family, mode: 'artwork', sharp })\n }\n return screens\n }\n const bytes = await readSourceBytes(source.located)\n if (!bytes) throw new PosReaderSplashRenderError('That image could not be read.')\n if (source.mode === 'artwork') {\n const facts = await posReaderSplashImageFacts(sharp, bytes)\n const problem = posReaderSplashArtworkProblem({\n contentType: facts?.contentType || null,\n bytes: bytes.length,\n width: facts?.width ?? 0,\n height: facts?.height ?? 0,\n })\n if (problem) throw new PosReaderSplashRenderError(problem)\n }\n for (const family of POS_READER_SPLASH_FAMILIES) {\n screens[family.id] = await renderPosReaderSplash({ source: bytes, family, mode: source.mode, sharp })\n }\n return screens\n}\n\n/** One screen uploaded to Stripe, keyed by its content so a retry makes no second file. */\nasync function uploadSplashFile(\n hostId: string,\n family: PosReaderSplashFamily,\n screen: RenderedPosReaderSplash,\n): Promise<string> {\n const form = new FormData()\n form.append('purpose', 'terminal_reader_splashscreen')\n const extension = screen.contentType === 'image/png' ? 'png' : 'jpg'\n form.append('file', new Blob([new Uint8Array(screen.body)], { type: screen.contentType }), `start-up-${family.id}.${extension}`)\n const response = await fetch('https://files.stripe.com/v1/files', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Stripe-Version': POS_STRIPE_VERSION,\n 'Idempotency-Key': `pos-splash-file:${hostId}:${family.id}:${digest(screen.body).slice(0, 32)}`,\n },\n body: form,\n })\n const body = (await response.json().catch(() => ({}))) as { id?: string; error?: { message?: string } }\n if (!response.ok || !body.id) {\n throw new PosReaderSplashRenderError(body.error?.message || 'Stripe did not accept the start-up screen.')\n }\n return String(body.id)\n}\n\n/** The org that owns the site, with its id on it (`planPosReaderSplash` reads it). */\nexport async function ownerOrgOf(hostId: string): Promise<Record<string, any> | null> {\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!owner?.org) return null\n return { ...(owner.org as Record<string, any>), id: String((owner.org as any).id ?? owner.orgId ?? '') }\n}\n\nexport interface PosReaderSplashApplyResult {\n outcome: 'unchanged' | 'applied' | 'cleared' | 'skipped' | 'failed'\n plan: PosReaderSplashPlan | null\n problem: string | null\n}\n\n/**\n * Puts the store's start-up screen on its readers' configuration when it\n * changed since it was last applied. Never throws: a failure is recorded on\n * the store's terminal document and returned, and the readers keep their\n * previous screen.\n */\nexport async function applyPosReaderSplash(\n hostId: string,\n options: { org?: Record<string, any> | null; platform?: PlatformSplashDoc; force?: boolean } = {},\n): Promise<PosReaderSplashApplyResult> {\n if (!posTerminalAvailable()) return { outcome: 'skipped', plan: null, problem: null }\n const ref = terminalConfigRef(hostId)\n const config = (await ref.get()).data() as Record<string, any> | undefined\n if (!config?.['stripeLocationId']) return { outcome: 'skipped', plan: null, problem: null }\n const org = options.org !== undefined ? options.org : await ownerOrgOf(hostId)\n const plan = await planPosReaderSplash(hostId, org, config, options.platform)\n const applied = typeof config['splashAppliedKey'] === 'string' ? (config['splashAppliedKey'] as string) : null\n // Nothing to show and nothing ever shown: the readers already show\n // Stripe's standard screen, and no Stripe call is needed to keep it.\n if (plan.key === 'none' && (applied === null || applied === 'none') && !options.force) {\n return { outcome: 'unchanged', plan, problem: plan.problem }\n }\n if (plan.key === applied && !options.force) return { outcome: 'unchanged', plan, problem: plan.problem }\n try {\n const params: Record<string, string> = {}\n if (plan.key === 'none' || !plan.source) {\n // An empty value unsets the device's screen on this configuration.\n for (const deviceType of POS_READER_SPLASH_DEVICE_TYPES) params[deviceType] = ''\n } else {\n const screens = await drawScreens(plan.source)\n for (const family of POS_READER_SPLASH_FAMILIES) {\n const fileId = await uploadSplashFile(hostId, family, screens[family.id])\n for (const deviceType of family.deviceTypes) params[`${deviceType}[splashscreen]`] = fileId\n }\n }\n const saved = await savePosReaderConfiguration(hostId, params, `splash:${digest(plan.key).slice(0, 24)}`)\n if (!saved.ok) throw new PosReaderSplashRenderError('Stripe did not accept the start-up screen. Try again.')\n await ref.set(\n {\n splashAppliedKey: plan.key,\n splashAppliedAtMs: Date.now(),\n splashProblem: firebaseAdmin.firestore.FieldValue.delete(),\n },\n { merge: true },\n )\n return { outcome: plan.key === 'none' ? 'cleared' : 'applied', plan, problem: plan.problem }\n } catch (error) {\n const problem =\n error instanceof PosReaderSplashRenderError ? error.message : 'The start-up screen could not be applied. Try again.'\n if (!(error instanceof PosReaderSplashRenderError)) console.error('[pos-reader-splash]', hostId, error)\n await ref.set({ splashProblem: problem, splashFailedAtMs: Date.now() }, { merge: true }).catch(() => undefined)\n return { outcome: 'failed', plan, problem }\n }\n}\n\n/*==========================================\n * `POST /api/commerce/pos-reader-splash`: the POS devices card's setting.\n *\n * status the store's choice and what it shows (re-applies a stale one)\n * save { choice, mediaScope?, mediaId? } — store and apply\n *\n * Gated like a sale (`authorizePosStaff`), as every reader control is.\n *=========================================*/\n\nfunction statusOf(\n plan: PosReaderSplashPlan | null,\n config: Record<string, any> | undefined,\n hostId: string,\n problem: string | null,\n): PosReaderSplashStatus {\n return {\n available: posTerminalAvailable(),\n choice: plan?.choice ?? null,\n effectiveChoice: plan?.effectiveChoice ?? 'platform',\n whiteLabel: plan?.whiteLabel ?? false,\n productName: plan?.productName ?? '',\n platformReady: plan?.platformReady ?? false,\n logoUrl: plan?.logoValue ? resolveMediaSrc(plan.logoValue, { hostId }) ?? null : null,\n image: plan?.image\n ? { ...plan.image, url: resolveMediaSrc(formatMediaRef(plan.image.mediaScope, plan.image.mediaId), { hostId }) ?? null }\n : null,\n locationReady: Boolean(config?.['stripeLocationId']),\n appliedAtMs: typeof config?.['splashAppliedAtMs'] === 'number' ? config['splashAppliedAtMs'] : null,\n problem: problem ?? (typeof config?.['splashProblem'] === 'string' ? config['splashProblem'] : null),\n }\n}\n\nasync function currentStatus(hostId: string, org: Record<string, any> | null): Promise<PosReaderSplashStatus> {\n const result = await applyPosReaderSplash(hostId, { org })\n const config = (await terminalConfigRef(hostId).get()).data() as Record<string, any> | undefined\n const plan = result.plan ?? (await planPosReaderSplash(hostId, org, config))\n return statusOf(plan, config, hostId, result.outcome === 'failed' ? result.problem : plan.problem)\n}\n\nexport const posReaderSplashHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = posRequestBody(req)\n const hostId = String(body['hostId'] ?? '')\n const gate = await authorizePosStaff(req, hostId)\n if ('error' in gate) return res.status(gate.status).json({ error: gate.error })\n const staff = gate.staff\n const org = staff.org ? { ...staff.org, id: staff.orgId } : null\n const action = String(body['action'] ?? '')\n try {\n if (action === 'status') return res.status(200).json(await currentStatus(hostId, org))\n if (action !== 'save') return res.status(400).json({ error: 'Unknown action' })\n if (!posTerminalAvailable()) return res.status(409).json({ error: 'Card readers are not available yet.' })\n const choice = body['choice']\n if (!isPosReaderSplashChoice(choice)) return res.status(400).json({ error: 'Choose what your readers show.' })\n const whiteLabel = checkEntitlement(staff.org as any, 'whiteLabel')\n if (choice === 'platform' && whiteLabel) {\n return res.status(400).json({ error: 'Choose your logo or your own image.' })\n }\n const setting: PosReaderSplashSetting = { choice, updatedAtMs: Date.now(), updatedBy: staff.uid }\n if (choice === 'image') {\n const mediaScope = String(body['mediaScope'] ?? '')\n const mediaId = String(body['mediaId'] ?? '')\n const located = await locateMediaAsset(mediaScope, mediaId, hostId, staff.orgId)\n if (!located) return res.status(400).json({ error: 'Choose an image from your media library.' })\n const contentType = String(located.doc['contentType'] ?? '')\n if (!POS_READER_SPLASH_SOURCE_TYPES.includes(contentType)) {\n return res.status(400).json({ error: 'Choose a PNG, JPEG or WebP image.' })\n }\n setting.mediaScope = mediaScope\n setting.mediaId = mediaId\n }\n await terminalConfigRef(hostId).set({ splash: setting }, { merge: true })\n return res.status(200).json(await currentStatus(hostId, org))\n } catch (error) {\n console.error('[pos-reader-splash]', action, error)\n return res.status(500).json({ error: 'The start-up screen could not be updated' })\n }\n}\n"],"names":["createHash","parseMediaCdnScope","formatMediaRef","isMediaCdnPath","MEDIA_CDN_ROUTE","mediaRefFromStorageUrl","parseMediaRef","resolveMediaSrc","checkEntitlement","resolveBrandingProfile","visibleToHost","firebaseAdmin","getOrgForHost","isPosReaderSplashChoice","POS_READER_SPLASH_DEVICE_TYPES","POS_READER_SPLASH_FAMILIES","POS_READER_SPLASH_SOURCE_MAX_BYTES","POS_READER_SPLASH_SOURCE_TYPES","posReaderSplashArtworkProblem","posReaderSplashEffectiveChoice","authorizePosStaff","posRequestBody","PosReaderSplashRenderError","posReaderSplashImageFacts","renderPosReaderSplash","POS_STRIPE_VERSION","posTerminalAvailable","savePosReaderConfiguration","RENDER_VERSION","POS_READER_SPLASH_SETTINGS_ID","firestore","app","terminalConfigRef","hostId","collection","doc","platformSplashRef","posReaderSplashBucket","storage","bucket","process","env","NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET","undefined","readPlatformSplash","snapshot","get","exists","data","platformSplashReady","Boolean","families","storagePath","platformSplashVersion","map","family","id","sha256","join","digest","value","update","cleanString","trim","locateMediaAsset","scopeSegment","mediaId","orgId","scope","test","owned","isOrg","scopeId","String","version","kind","locateSource","stored","ref","rest","slice","length","split","slash","indexOf","address","url","readAssetBytes","asset","mediaCdnServeBlock","mediaStoragePathInScope","blocked","contentSha256","contentHash","objectPath","base","buffer","file","download","FETCH_TIMEOUT_MS","fetchPublicImage","createPinnedDispatcher","resolvePublicIp","pinnedIp","URL","hostname","dispatcher","controller","AbortController","timeout","setTimeout","abort","response","init","redirect","signal","fetch","ok","headers","declared","Number","Buffer","from","arrayBuffer","clearTimeout","close","catch","readSourceBytes","source","workspaceLogoValue","org","whiteLabel","brand","NO_LOGO","NO_IMAGE","planPosReaderSplash","config","platform","productName","setting","choice","effectiveChoice","logoValue","image","mediaScope","platformReady","key","problem","mode","located","loadSharpFactory","loadSharp","drawScreens","sharp","screens","read","large","largeBody","own","body","contentType","bytes","facts","width","height","uploadSplashFile","screen","form","FormData","append","extension","Blob","Uint8Array","type","method","Authorization","STRIPE_SECRET_KEY","json","error","message","ownerOrgOf","owner","applyPosReaderSplash","options","outcome","plan","applied","force","params","deviceType","fileId","deviceTypes","saved","set","splashAppliedKey","splashAppliedAtMs","Date","now","splashProblem","FieldValue","delete","merge","console","splashFailedAtMs","statusOf","available","logoUrl","locationReady","appliedAtMs","currentStatus","result","posReaderSplashHandler","req","res","status","gate","staff","action","updatedAtMs","updatedBy","uid","includes","splash"],"mappings":"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"}
|
|
@@ -93,7 +93,8 @@ export declare function posSaleSummary(order: PosLiftedOrder): {
|
|
|
93
93
|
*
|
|
94
94
|
* Pure, so the spec can hold the arithmetic: the fee is every card payment's
|
|
95
95
|
* own `application_fee_amount` plus the take the cards did not carry, the tip
|
|
96
|
-
* is summed beside the total and never inside it.
|
|
96
|
+
* is summed beside the total and never inside it. Cash and gift-card parts
|
|
97
|
+
* carry no take at all (AGL-4036), so they add nothing to either half.
|
|
97
98
|
*/
|
|
98
99
|
export declare function posCompletionPatch(order: PosLiftedOrder, payments: CommerceModel.OrderPayment[], nowMs: number): {
|
|
99
100
|
patch: Record<string, unknown>;
|
|
@@ -78,7 +78,8 @@ import { ORDER_PAID_EVENT } from "../model/order-events.js";
|
|
|
78
78
|
*
|
|
79
79
|
* Pure, so the spec can hold the arithmetic: the fee is every card payment's
|
|
80
80
|
* own `application_fee_amount` plus the take the cards did not carry, the tip
|
|
81
|
-
* is summed beside the total and never inside it.
|
|
81
|
+
* is summed beside the total and never inside it. Cash and gift-card parts
|
|
82
|
+
* carry no take at all (AGL-4036), so they add nothing to either half.
|
|
82
83
|
*/ export function posCompletionPatch(order, payments, nowMs) {
|
|
83
84
|
var _ref, _order_posTakeFeeCents, _order_totals;
|
|
84
85
|
var _order_totals1;
|
|
@@ -88,6 +89,7 @@ import { ORDER_PAID_EVENT } from "../model/order-events.js";
|
|
|
88
89
|
const takeFeeCents = Math.max(0, Math.round(Number((_order_posTakeFeeCents = order.posTakeFeeCents) != null ? _order_posTakeFeeCents : 0)));
|
|
89
90
|
const invoiceTakeCents = CommerceModel.posInvoiceTakeCents({
|
|
90
91
|
takeFeeCents,
|
|
92
|
+
totalCents,
|
|
91
93
|
payments
|
|
92
94
|
});
|
|
93
95
|
const settled = payments.filter((payment)=>payment.status === 'succeeded');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-sale.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'crypto'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport * as CommerceModel from '../model'\nimport { alertLowStockCrossing } from './low-stock'\nimport { decrementVariantStock } from './reserve-stock'\nimport { releasePromotionHold, settlePromotionSlot } from './promotion-hold'\nimport { offlineFeeMonthKey } from './pos-fee-month'\nimport { resetPosDisplay } from './pos-display'\nimport { notifyOrderBuyer, sendOrderReceipt } from './order-notifications'\nimport { raiseOrderEvent } from './order-events'\nimport { ORDER_PAID_EVENT } from '../model/order-events'\n\n/*==========================================\n * AN OPEN REGISTER SALE (AGL-3607).\n *\n * A split-tender sale is an order written `pending` with an empty\n * `payments[]`, priced once by `pos-order.ts` (`payment: 'open'`). Each tender\n * then adds one payment through {@link applyPosPayment}, which is the ONLY\n * writer of the ledger and runs every change in one transaction on the order\n * document. When the settled payments cover the total, the same transaction\n * flips the order to `paid` and accrues the invoiced part of the fee, so a\n * sale is completed exactly once however many webhooks, polls and retries\n * race to report its last payment. Stock, the promotion slot, the contact and\n * the receipt follow from that one flip ({@link completePosSale}).\n *=========================================*/\n\nexport type PosLiftedOrder = CommerceModel.HostOrder & {\n $id: string\n /** The sale's platform take, before any card processing (AGL-2111). */\n posTakeFeeCents?: number\n /** The org the invoiced part of the take accrues to. */\n posFeeOrgId?: string\n /** The promotion slot this sale holds until it completes (AGL-305). */\n discountId?: string\n discountHoldKey?: string\n paymentIntentIds?: string[]\n reservationId?: string\n}\n\n/**\n * A payment's id, derived from the register's attempt key so a retried tap\n * finds the payment it already started instead of starting a second one.\n */\nexport function posPaymentId(orderId: string, attemptKey: string): string {\n return `pay_${createHash('sha256')\n .update(`${orderId}:${attemptKey}`)\n .digest('hex')\n .slice(0, 24)}`\n}\n\n/** What one ledger change decided. */\nexport type PosPaymentStep =\n | { kind: 'refuse'; status: number; error: string }\n /** Nothing to change: the payment is already in the state asked for. */\n | { kind: 'keep' }\n | {\n kind: 'put'\n payment: CommerceModel.OrderPayment\n /** A timeline line for the order, when the change is worth one. */\n event?: { event: string; detail?: string }\n }\n\nexport type PosPaymentOutcome =\n | { ok: false; status: number; error: string }\n | {\n ok: true\n order: PosLiftedOrder\n payment: CommerceModel.OrderPayment | null\n /** True only on the call whose change completed the sale. */\n completed: boolean\n changed: boolean\n }\n\n/** The open sale's ledger, as the register reads it back after any change. */\nexport function posSaleSummary(order: PosLiftedOrder) {\n const payments = CommerceModel.orderPayments(order)\n const totalCents = Number(order.totals?.totalCents ?? 0)\n return {\n orderId: order.$id,\n status: order.status,\n totalCents,\n paidCents: CommerceModel.posSettledCents(payments),\n dueCents: CommerceModel.posBalanceDueCents(totalCents, payments),\n tenderableCents: CommerceModel.posTenderableCents(totalCents, payments),\n tipCents: CommerceModel.posTipCents(payments),\n payments: payments.map((payment) => ({\n id: payment.id,\n method: payment.method,\n amountCents: payment.amountCents,\n ...(payment.tipCents ? { tipCents: payment.tipCents } : {}),\n status: payment.status,\n ...(payment.cardBrand ? { cardBrand: payment.cardBrand } : {}),\n ...(payment.last4 ? { last4: payment.last4 } : {}),\n ...(payment.changeCents != null ? { changeCents: payment.changeCents } : {}),\n ...(payment.checkoutUrl && payment.status === 'pending'\n ? { checkoutUrl: payment.checkoutUrl }\n : {}),\n ...(payment.readerId ? { readerId: payment.readerId } : {}),\n // What a store-credit payment's provider calls it (AGL-3640).\n ...(payment.creditLabel ? { creditLabel: payment.creditLabel } : {}),\n ...(payment.failureMessage ? { failureMessage: payment.failureMessage } : {}),\n ...(payment.livemode != null ? { livemode: payment.livemode } : {}),\n })),\n }\n}\n\n/**\n * The fields that complete a sale, when its settled payments now cover the\n * total; null when the balance is still open or the sale is not pending.\n *\n * Pure, so the spec can hold the arithmetic: the fee is every card payment's\n * own `application_fee_amount` plus the take the cards did not carry, the tip\n * is summed beside the total and never inside it.\n */\nexport function posCompletionPatch(\n order: PosLiftedOrder,\n payments: CommerceModel.OrderPayment[],\n nowMs: number,\n): {\n patch: Record<string, unknown>\n invoiceTakeCents: number\n} | null {\n if (order.status !== 'pending') return null\n const totalCents = Number(order.totals?.totalCents ?? 0)\n if (CommerceModel.posBalanceDueCents(totalCents, payments) > 0) return null\n const takeFeeCents = Math.max(0, Math.round(Number(order.posTakeFeeCents ?? 0)))\n const invoiceTakeCents = CommerceModel.posInvoiceTakeCents({ takeFeeCents, payments })\n const settled = payments.filter((payment) => payment.status === 'succeeded')\n const cardFeeCents = settled\n .filter((payment) => CommerceModel.isCardPaymentMethod(payment.method))\n .reduce((sum, payment) => sum + Math.max(0, Math.round(Number(payment.feeCents ?? 0))), 0)\n const feeCents = cardFeeCents + invoiceTakeCents\n const tipCents = CommerceModel.posTipCents(payments)\n const intents = settled\n .map((payment) => payment.paymentIntentId)\n .filter((id): id is string => Boolean(id))\n const tenders = settled.map((payment) => CommerceModel.describeOrderPayment(payment))\n const overpaid = CommerceModel.posSettledCents(payments) - totalCents\n return {\n invoiceTakeCents,\n patch: {\n status: 'paid',\n paidAtMs: nowMs,\n totals: {\n ...(order.totals ?? {}),\n feeCents,\n ...(tipCents > 0 ? { tipCents } : {}),\n },\n ...(feeCents > 0 ? { feeCollection: CommerceModel.posFeeCollection(payments) } : {}),\n ...(intents.length ? { paymentIntentIds: intents } : {}),\n // The single-charge readers (refund, dispute lookup) key on this; a\n // sale with one card payment keeps working with every one of them.\n ...(intents.length === 1 ? { paymentIntentId: intents[0] } : {}),\n ...(settled.some((payment) => payment.livemode === false) ? { livemode: false } : {}),\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'paid',\n `Paid — ${tenders.join(', ')}` +\n (overpaid > 0\n ? `. Overpaid by $${(overpaid / 100).toFixed(2)}: refund the difference.`\n : ''),\n nowMs,\n ),\n },\n }\n}\n\n/** The order document, lifted, with its id. */\nfunction liftOrder(id: string, data: unknown): PosLiftedOrder {\n return {\n ...(CommerceModel.liftLegacyOrder((data ?? {}) as any) as any),\n $id: id,\n }\n}\n\n/**\n * The ONE writer of a sale's ledger. Runs `prepare` (extra reads) and\n * `decide` inside one transaction on the order, writes the payment, and\n * completes the sale in the same commit when the balance reaches zero.\n */\nexport async function applyPosPayment<C = undefined>(options: {\n hostId: string\n orderId: string\n paymentId: string\n /** Reads beyond the order (a gift card, a stay), before any write. */\n prepare?: (transaction: any) => Promise<C>\n decide: (sale: {\n order: PosLiftedOrder\n payments: CommerceModel.OrderPayment[]\n existing: CommerceModel.OrderPayment | undefined\n context: C\n }) => PosPaymentStep\n /** Writes beyond the order, in the same transaction, for a `put`. */\n write?: (\n transaction: any,\n context: C,\n payment: CommerceModel.OrderPayment,\n previous: CommerceModel.OrderPayment | undefined,\n ) => void\n nowMs?: number\n}): Promise<PosPaymentOutcome> {\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = firestore\n .collection('hosts')\n .doc(options.hostId)\n .collection('orders')\n .doc(options.orderId)\n const nowMs = options.nowMs ?? Date.now()\n const outcome = await firestore.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) {\n return { ok: false as const, status: 404, error: 'Unknown sale' }\n }\n const order = liftOrder(orderRef.id, snapshot.data())\n if (order.channel !== 'pos' || !Array.isArray(order.payments)) {\n return { ok: false as const, status: 409, error: 'This order is not an open register sale' }\n }\n const context = (options.prepare\n ? await options.prepare(transaction)\n : undefined) as C\n const payments = CommerceModel.orderPayments(order)\n const existing = payments.find((payment) => payment.id === options.paymentId)\n const step = options.decide({ order, payments, existing, context })\n if (step.kind === 'refuse') {\n return { ok: false as const, status: step.status, error: step.error }\n }\n if (step.kind === 'keep') {\n return {\n ok: true as const,\n order,\n payment: existing ?? null,\n completed: false,\n changed: false,\n }\n }\n const nextPayments = existing\n ? payments.map((payment) => (payment.id === step.payment.id ? step.payment : payment))\n : [...payments, step.payment]\n let next: PosLiftedOrder = { ...order, payments: nextPayments }\n if (step.event) {\n next = {\n ...next,\n timeline: CommerceModel.appendOrderEvent(\n order,\n step.event.event,\n step.event.detail,\n nowMs,\n ),\n }\n }\n const completion = posCompletionPatch(next, nextPayments, nowMs)\n const write: Record<string, unknown> = {\n payments: nextPayments,\n ...(next.timeline !== order.timeline ? { timeline: next.timeline } : {}),\n }\n if (completion) {\n Object.assign(write, completion.patch)\n next = { ...next, ...(completion.patch as Partial<PosLiftedOrder>) }\n Object.assign(write, CommerceModel.orderListFields(next, orderRef.id))\n // ACCRUED IN THE SAME COMMIT AS THE FLIP (the AGL-2111 rule): the part\n // of the take no card payout carried goes on the org's invoice, and an\n // order and the fee it owes cannot diverge.\n const feeOrgId = String(order.posFeeOrgId ?? '')\n if (completion.invoiceTakeCents > 0 && feeOrgId) {\n const month = offlineFeeMonthKey(new Date(nowMs))\n transaction.set(\n firestore.collection('orgs').doc(feeOrgId).collection('offlineFees').doc(month),\n {\n month,\n feeCents: firebaseAdmin.firestore.FieldValue.increment(completion.invoiceTakeCents),\n orders: firebaseAdmin.firestore.FieldValue.increment(1),\n },\n { merge: true },\n )\n }\n } else if (\n order.status === 'paid' &&\n step.payment.status === 'succeeded' &&\n existing?.status !== 'succeeded'\n ) {\n // MONEY THAT ARRIVED AFTER THE SALE WAS ALREADY PAID: a QR page paid\n // after the cashier took another tender. It is recorded, never\n // dropped, and said on the order so the merchant can refund it.\n write['timeline'] = CommerceModel.appendOrderEvent(\n next,\n 'pos-overpaid',\n `${CommerceModel.describeOrderPayment(step.payment)} arrived after the ` +\n 'sale was already paid. Refund it from Stripe.',\n nowMs,\n )\n }\n transaction.set(orderRef, write, { merge: true })\n options.write?.(transaction, context, step.payment, existing)\n return {\n ok: true as const,\n order: next,\n payment: step.payment,\n completed: Boolean(completion),\n changed: true,\n }\n })\n if (outcome.ok && outcome.completed) {\n await completePosSale(options.hostId, outcome.order)\n }\n return outcome\n}\n\n/*==========================================\n * \"A REGISTER SALE COMPLETED\" — the one point every POS sale passes through\n * (AGL-3607), whichever way it was paid: the ledger's last payment\n * ({@link completePosSale}), a single-tender cash or room sale\n * (`pos-order.ts`), or a legacy QR sale paid through the `commerce-draft`\n * webhook branch. Peripherals (a cloud receipt printer, a cash drawer kick)\n * and anything else that must follow a sale subscribe here rather than\n * patching each path.\n *=========================================*/\n\nexport interface PosSaleCompletedEvent {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n /**\n * Rung while the register was offline and synced later (AGL-3625): the\n * customer has already left, so nothing at the counter should react now.\n */\n offline?: boolean\n}\n\nexport type PosSaleCompletedListener = (event: PosSaleCompletedEvent) => void | Promise<void>\n\nconst saleCompletedListeners = new Set<PosSaleCompletedListener>()\n\n/** Subscribes to completed register sales; returns the unsubscribe. */\nexport function onPosSaleCompleted(listener: PosSaleCompletedListener): () => void {\n saleCompletedListeners.add(listener)\n return () => saleCompletedListeners.delete(listener)\n}\n\n/**\n * Tells every listener a register sale completed. Awaited, and never throws:\n * the money is already taken, so a listener's failure is logged, not\n * returned to a cashier who would ring the sale again.\n */\nexport async function notifyPosSaleCompleted(event: PosSaleCompletedEvent): Promise<void> {\n for (const listener of [...saleCompletedListeners]) {\n try {\n await listener(event)\n } catch (error) {\n console.error('[pos-sale] sale-completed listener failed', event.orderId, error)\n }\n }\n}\n\n/** Reads the sale as it stands, for the register's status polls. */\nexport async function readPosSale(\n hostId: string,\n orderId: string,\n): Promise<PosLiftedOrder | null> {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n .get()\n if (!snapshot.exists) return null\n return liftOrder(snapshot.id, snapshot.data())\n}\n\n/**\n * Everything that follows a sale being paid, run once by the call whose\n * commit completed it. Each step is guarded on its own and none can undo the\n * sale: the money is taken, so a failure here is logged and said on the order\n * rather than thrown back at a cashier who would ring it again.\n */\nexport async function completePosSale(\n hostId: string,\n order: PosLiftedOrder,\n): Promise<void> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const orderRef = hostRef.collection('orders').doc(order.$id)\n const notes: Array<{ event: string; detail: string }> = []\n\n // The promotion's slot becomes a redemption only now: a sale that was\n // voided half-paid never counted against the merchant's cap (AGL-305).\n if (order.discountId && order.discountHoldKey) {\n const settled = await settlePromotionSlot({\n firestore,\n ref: hostRef.collection('discounts').doc(order.discountId),\n holdKey: order.discountHoldKey,\n label: `pos discount ${order.discountId}`,\n })\n if (settled === 'missing' || settled === 'error') {\n notes.push({\n event: 'redemption-orphaned',\n detail:\n `Discount ${order.discountId} was applied to this sale but its ` +\n 'redemption could not be counted against its limit.',\n })\n }\n }\n\n // Stock comes off exactly once, here, compounding across lines of one\n // product (AGL-1828) and atomically against other channels (AGL-2320).\n const products = new Map<string, CommerceModel.HostProduct>()\n for (const line of order.lineItems ?? []) {\n try {\n if (!products.has(line.productId)) {\n const snapshot = await hostRef.collection('products').doc(line.productId).get()\n if (!snapshot.exists) continue\n products.set(line.productId, CommerceModel.liftLegacyProduct(snapshot.data() as any))\n }\n const product = products.get(line.productId) as CommerceModel.HostProduct\n const variantId = line.variantId ?? product.variants[0]?.id\n const tracked = product.variants.some(\n (variant) => variant.id === variantId && variant.inventory != null,\n )\n if (!variantId || !tracked) continue\n const moved = await decrementVariantStock({\n firestore,\n hostRef,\n hostId,\n productId: line.productId,\n variantId,\n quantity: line.quantity,\n locationId: order.locationId || undefined,\n ledger: { reason: 'sale', orderId: order.$id },\n })\n if (!moved.before || !moved.after) continue\n products.set(line.productId, moved.after)\n alertLowStockCrossing(hostId, moved.before, moved.after)\n } catch (error) {\n console.error('[pos-sale] stock decrement failed', order.$id, line.productId, error)\n }\n }\n\n const receipt = order.receiptRequest\n const receiptEmail = receipt?.channel === 'email' ? String(receipt.to ?? '') : ''\n const contactEmail = receiptEmail || String(order.customerEmail ?? '')\n const marketingOptIn = Boolean((receipt as { marketingOptIn?: boolean } | undefined)?.marketingOptIn)\n const totalCents = Number(order.totals?.totalCents ?? 0)\n if (contactEmail) {\n await recordCapturedContact({\n orgId: '',\n hostId,\n identity: { email: contactEmail },\n surface: 'relationship',\n lifecycleFloor: 'customer',\n // The customer's own tick on the display, the same opt-in the online\n // checkout records (AGL-3608); an unticked box records nothing.\n ...(marketingOptIn ? { marketingConsent: true } : {}),\n purchaseCents: totalCents,\n interaction: {\n source: 'order',\n refId: order.$id,\n summary: `In-store purchase ($${(totalCents / 100).toFixed(2)})`,\n },\n }).catch((error: unknown) => {\n console.error('[pos-sale] contact capture failed', error)\n })\n }\n\n // The receipt and the paid event, through the same doors every sale uses\n // (AGL-3610, AGL-3611): the buyer-notification door claims each message\n // per channel, so a second completion path can never mail twice, and it\n // texts the receipt when the customer chose a text and a provider is on.\n if ((receipt?.channel === 'email' || receipt?.channel === 'sms') && receipt.to) {\n // The customer named where it goes: it goes there, whatever else the\n // order holds (AGL-3608).\n const sent = await deliverChosenPosReceipt(\n { hostId, orderId: order.$id },\n { channel: receipt.channel, to: receipt.to, orderEmail: String(order.customerEmail ?? '') },\n )\n if (sent !== 'sent') {\n notes.push({\n event: 'receipt-unsent',\n detail: `The ${receipt.channel === 'sms' ? 'text' : 'email'} receipt could not be sent.`,\n })\n }\n } else if (contactEmail) {\n await notifyOrderBuyer({ hostId, orderId: order.$id }, 'receipt', { email: contactEmail })\n }\n await raiseOrderEvent(ORDER_PAID_EVENT, { hostId, orderId: order.$id, key: 'paid' }).catch(\n (error: unknown) => console.error('[pos-sale] order.paid event failed', error),\n )\n\n if (notes.length) {\n await firestore\n .runTransaction(async (transaction: any) => {\n const fresh = liftOrder(orderRef.id, (await transaction.get(orderRef)).data())\n let timeline = fresh.timeline ?? []\n for (const note of notes) {\n timeline = CommerceModel.appendOrderEvent({ timeline }, note.event, note.detail)\n }\n transaction.set(orderRef, { timeline }, { merge: true })\n })\n .catch((error: unknown) => console.error('[pos-sale] note failed', error))\n }\n\n await notifyPosSaleCompleted({ hostId, orderId: order.$id, order })\n\n // The customer screen says thank you and forgets the customer: the state\n // is rewritten whole, so no address or phone outlives the sale (AGL-3608).\n if (order.registerId) {\n await resetPosDisplay(hostId, order.registerId).catch((error: unknown) =>\n console.error('[pos-sale] display reset failed', error),\n )\n }\n}\n\n/**\n * Sends a receipt the customer (or the cashier for them) explicitly asked\n * for, to the address or number they gave (AGL-3607, AGL-3608).\n *\n * It goes through the buyer-notification door first, so the order's\n * once-per-channel marker is claimed and a completion that races this call\n * cannot send a second copy. The door can skip it, though, for reasons that\n * do not apply to a request made at the counter: the store switched\n * automatic receipts or texts off, a receipt already went out on that\n * channel to an earlier address, or the door would use the email the order\n * was opened with rather than the one just typed. In each of those cases the\n * receipt is sent explicitly, as the order dialog's \"Resend receipt\" does.\n */\nexport async function deliverChosenPosReceipt(\n ref: { hostId: string; orderId: string },\n input: { channel: 'email' | 'sms'; to: string; orderEmail?: string },\n): Promise<'sent' | 'failed' | 'not_configured'> {\n const orderEmail = String(input.orderEmail ?? '').trim().toLowerCase()\n const to = input.to.trim()\n const doorAddressesIt = input.channel === 'sms' || !orderEmail || orderEmail === to.toLowerCase()\n if (doorAddressesIt) {\n const door = await notifyOrderBuyer(ref, 'receipt', input.channel === 'email' ? { email: to } : {})\n const entry =\n door.outcome === 'handled'\n ? door.channels.find((channel) => channel.channel === input.channel)\n : undefined\n if (entry?.outcome === 'sent') return 'sent'\n if (entry?.outcome === 'failed') return 'failed'\n // Already sent to this very address: nothing more to do.\n if (entry?.outcome === 'already' && input.channel === 'email') return 'sent'\n }\n const explicit = await sendOrderReceipt(ref, { channel: input.channel, to })\n if (explicit.outcome === 'sent') return 'sent'\n return explicit.outcome === 'not_configured' ? 'not_configured' : 'failed'\n}\n\n/**\n * Hands back a promotion slot an open sale was holding, when the sale is\n * voided rather than paid.\n */\nexport async function releasePosSaleDiscount(\n hostId: string,\n order: PosLiftedOrder,\n): Promise<void> {\n if (!order.discountId || !order.discountHoldKey) return\n await releasePromotionHold(\n firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('discounts')\n .doc(order.discountId),\n order.discountHoldKey,\n )\n}\n"],"names":["createHash","firebaseAdmin","recordCapturedContact","CommerceModel","alertLowStockCrossing","decrementVariantStock","releasePromotionHold","settlePromotionSlot","offlineFeeMonthKey","resetPosDisplay","notifyOrderBuyer","sendOrderReceipt","raiseOrderEvent","ORDER_PAID_EVENT","posPaymentId","orderId","attemptKey","update","digest","slice","posSaleSummary","order","payments","orderPayments","totalCents","Number","totals","$id","status","paidCents","posSettledCents","dueCents","posBalanceDueCents","tenderableCents","posTenderableCents","tipCents","posTipCents","map","payment","id","method","amountCents","cardBrand","last4","changeCents","checkoutUrl","readerId","creditLabel","failureMessage","livemode","posCompletionPatch","nowMs","takeFeeCents","Math","max","round","posTakeFeeCents","invoiceTakeCents","posInvoiceTakeCents","settled","filter","cardFeeCents","isCardPaymentMethod","reduce","sum","feeCents","intents","paymentIntentId","Boolean","tenders","describeOrderPayment","overpaid","patch","paidAtMs","feeCollection","posFeeCollection","length","paymentIntentIds","some","timeline","appendOrderEvent","join","toFixed","liftOrder","data","liftLegacyOrder","applyPosPayment","options","firestore","app","orderRef","collection","doc","hostId","Date","now","outcome","runTransaction","transaction","snapshot","get","exists","ok","error","channel","Array","isArray","context","prepare","undefined","existing","find","paymentId","step","decide","kind","completed","changed","nextPayments","next","event","detail","completion","write","Object","assign","orderListFields","feeOrgId","String","posFeeOrgId","month","set","FieldValue","increment","orders","merge","completePosSale","saleCompletedListeners","Set","onPosSaleCompleted","listener","add","delete","notifyPosSaleCompleted","console","readPosSale","receipt","hostRef","notes","discountId","discountHoldKey","ref","holdKey","label","push","products","Map","line","lineItems","product","has","productId","liftLegacyProduct","variantId","variants","tracked","variant","inventory","moved","quantity","locationId","ledger","reason","before","after","receiptRequest","receiptEmail","to","contactEmail","customerEmail","marketingOptIn","orgId","identity","email","surface","lifecycleFloor","marketingConsent","purchaseCents","interaction","source","refId","summary","catch","sent","deliverChosenPosReceipt","orderEmail","key","fresh","note","registerId","input","trim","toLowerCase","doorAddressesIt","door","entry","channels","explicit","releasePosSaleDiscount"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-sale.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'crypto'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport * as CommerceModel from '../model'\nimport { alertLowStockCrossing } from './low-stock'\nimport { decrementVariantStock } from './reserve-stock'\nimport { releasePromotionHold, settlePromotionSlot } from './promotion-hold'\nimport { offlineFeeMonthKey } from './pos-fee-month'\nimport { resetPosDisplay } from './pos-display'\nimport { notifyOrderBuyer, sendOrderReceipt } from './order-notifications'\nimport { raiseOrderEvent } from './order-events'\nimport { ORDER_PAID_EVENT } from '../model/order-events'\n\n/*==========================================\n * AN OPEN REGISTER SALE (AGL-3607).\n *\n * A split-tender sale is an order written `pending` with an empty\n * `payments[]`, priced once by `pos-order.ts` (`payment: 'open'`). Each tender\n * then adds one payment through {@link applyPosPayment}, which is the ONLY\n * writer of the ledger and runs every change in one transaction on the order\n * document. When the settled payments cover the total, the same transaction\n * flips the order to `paid` and accrues the invoiced part of the fee, so a\n * sale is completed exactly once however many webhooks, polls and retries\n * race to report its last payment. Stock, the promotion slot, the contact and\n * the receipt follow from that one flip ({@link completePosSale}).\n *=========================================*/\n\nexport type PosLiftedOrder = CommerceModel.HostOrder & {\n $id: string\n /** The sale's platform take, before any card processing (AGL-2111). */\n posTakeFeeCents?: number\n /** The org the invoiced part of the take accrues to. */\n posFeeOrgId?: string\n /** The promotion slot this sale holds until it completes (AGL-305). */\n discountId?: string\n discountHoldKey?: string\n paymentIntentIds?: string[]\n reservationId?: string\n}\n\n/**\n * A payment's id, derived from the register's attempt key so a retried tap\n * finds the payment it already started instead of starting a second one.\n */\nexport function posPaymentId(orderId: string, attemptKey: string): string {\n return `pay_${createHash('sha256')\n .update(`${orderId}:${attemptKey}`)\n .digest('hex')\n .slice(0, 24)}`\n}\n\n/** What one ledger change decided. */\nexport type PosPaymentStep =\n | { kind: 'refuse'; status: number; error: string }\n /** Nothing to change: the payment is already in the state asked for. */\n | { kind: 'keep' }\n | {\n kind: 'put'\n payment: CommerceModel.OrderPayment\n /** A timeline line for the order, when the change is worth one. */\n event?: { event: string; detail?: string }\n }\n\nexport type PosPaymentOutcome =\n | { ok: false; status: number; error: string }\n | {\n ok: true\n order: PosLiftedOrder\n payment: CommerceModel.OrderPayment | null\n /** True only on the call whose change completed the sale. */\n completed: boolean\n changed: boolean\n }\n\n/** The open sale's ledger, as the register reads it back after any change. */\nexport function posSaleSummary(order: PosLiftedOrder) {\n const payments = CommerceModel.orderPayments(order)\n const totalCents = Number(order.totals?.totalCents ?? 0)\n return {\n orderId: order.$id,\n status: order.status,\n totalCents,\n paidCents: CommerceModel.posSettledCents(payments),\n dueCents: CommerceModel.posBalanceDueCents(totalCents, payments),\n tenderableCents: CommerceModel.posTenderableCents(totalCents, payments),\n tipCents: CommerceModel.posTipCents(payments),\n payments: payments.map((payment) => ({\n id: payment.id,\n method: payment.method,\n amountCents: payment.amountCents,\n ...(payment.tipCents ? { tipCents: payment.tipCents } : {}),\n status: payment.status,\n ...(payment.cardBrand ? { cardBrand: payment.cardBrand } : {}),\n ...(payment.last4 ? { last4: payment.last4 } : {}),\n ...(payment.changeCents != null ? { changeCents: payment.changeCents } : {}),\n ...(payment.checkoutUrl && payment.status === 'pending'\n ? { checkoutUrl: payment.checkoutUrl }\n : {}),\n ...(payment.readerId ? { readerId: payment.readerId } : {}),\n // What a store-credit payment's provider calls it (AGL-3640).\n ...(payment.creditLabel ? { creditLabel: payment.creditLabel } : {}),\n ...(payment.failureMessage ? { failureMessage: payment.failureMessage } : {}),\n ...(payment.livemode != null ? { livemode: payment.livemode } : {}),\n })),\n }\n}\n\n/**\n * The fields that complete a sale, when its settled payments now cover the\n * total; null when the balance is still open or the sale is not pending.\n *\n * Pure, so the spec can hold the arithmetic: the fee is every card payment's\n * own `application_fee_amount` plus the take the cards did not carry, the tip\n * is summed beside the total and never inside it. Cash and gift-card parts\n * carry no take at all (AGL-4036), so they add nothing to either half.\n */\nexport function posCompletionPatch(\n order: PosLiftedOrder,\n payments: CommerceModel.OrderPayment[],\n nowMs: number,\n): {\n patch: Record<string, unknown>\n invoiceTakeCents: number\n} | null {\n if (order.status !== 'pending') return null\n const totalCents = Number(order.totals?.totalCents ?? 0)\n if (CommerceModel.posBalanceDueCents(totalCents, payments) > 0) return null\n const takeFeeCents = Math.max(0, Math.round(Number(order.posTakeFeeCents ?? 0)))\n const invoiceTakeCents = CommerceModel.posInvoiceTakeCents({ takeFeeCents, totalCents, payments })\n const settled = payments.filter((payment) => payment.status === 'succeeded')\n const cardFeeCents = settled\n .filter((payment) => CommerceModel.isCardPaymentMethod(payment.method))\n .reduce((sum, payment) => sum + Math.max(0, Math.round(Number(payment.feeCents ?? 0))), 0)\n const feeCents = cardFeeCents + invoiceTakeCents\n const tipCents = CommerceModel.posTipCents(payments)\n const intents = settled\n .map((payment) => payment.paymentIntentId)\n .filter((id): id is string => Boolean(id))\n const tenders = settled.map((payment) => CommerceModel.describeOrderPayment(payment))\n const overpaid = CommerceModel.posSettledCents(payments) - totalCents\n return {\n invoiceTakeCents,\n patch: {\n status: 'paid',\n paidAtMs: nowMs,\n totals: {\n ...(order.totals ?? {}),\n feeCents,\n ...(tipCents > 0 ? { tipCents } : {}),\n },\n ...(feeCents > 0 ? { feeCollection: CommerceModel.posFeeCollection(payments) } : {}),\n ...(intents.length ? { paymentIntentIds: intents } : {}),\n // The single-charge readers (refund, dispute lookup) key on this; a\n // sale with one card payment keeps working with every one of them.\n ...(intents.length === 1 ? { paymentIntentId: intents[0] } : {}),\n ...(settled.some((payment) => payment.livemode === false) ? { livemode: false } : {}),\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'paid',\n `Paid — ${tenders.join(', ')}` +\n (overpaid > 0\n ? `. Overpaid by $${(overpaid / 100).toFixed(2)}: refund the difference.`\n : ''),\n nowMs,\n ),\n },\n }\n}\n\n/** The order document, lifted, with its id. */\nfunction liftOrder(id: string, data: unknown): PosLiftedOrder {\n return {\n ...(CommerceModel.liftLegacyOrder((data ?? {}) as any) as any),\n $id: id,\n }\n}\n\n/**\n * The ONE writer of a sale's ledger. Runs `prepare` (extra reads) and\n * `decide` inside one transaction on the order, writes the payment, and\n * completes the sale in the same commit when the balance reaches zero.\n */\nexport async function applyPosPayment<C = undefined>(options: {\n hostId: string\n orderId: string\n paymentId: string\n /** Reads beyond the order (a gift card, a stay), before any write. */\n prepare?: (transaction: any) => Promise<C>\n decide: (sale: {\n order: PosLiftedOrder\n payments: CommerceModel.OrderPayment[]\n existing: CommerceModel.OrderPayment | undefined\n context: C\n }) => PosPaymentStep\n /** Writes beyond the order, in the same transaction, for a `put`. */\n write?: (\n transaction: any,\n context: C,\n payment: CommerceModel.OrderPayment,\n previous: CommerceModel.OrderPayment | undefined,\n ) => void\n nowMs?: number\n}): Promise<PosPaymentOutcome> {\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = firestore\n .collection('hosts')\n .doc(options.hostId)\n .collection('orders')\n .doc(options.orderId)\n const nowMs = options.nowMs ?? Date.now()\n const outcome = await firestore.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) {\n return { ok: false as const, status: 404, error: 'Unknown sale' }\n }\n const order = liftOrder(orderRef.id, snapshot.data())\n if (order.channel !== 'pos' || !Array.isArray(order.payments)) {\n return { ok: false as const, status: 409, error: 'This order is not an open register sale' }\n }\n const context = (options.prepare\n ? await options.prepare(transaction)\n : undefined) as C\n const payments = CommerceModel.orderPayments(order)\n const existing = payments.find((payment) => payment.id === options.paymentId)\n const step = options.decide({ order, payments, existing, context })\n if (step.kind === 'refuse') {\n return { ok: false as const, status: step.status, error: step.error }\n }\n if (step.kind === 'keep') {\n return {\n ok: true as const,\n order,\n payment: existing ?? null,\n completed: false,\n changed: false,\n }\n }\n const nextPayments = existing\n ? payments.map((payment) => (payment.id === step.payment.id ? step.payment : payment))\n : [...payments, step.payment]\n let next: PosLiftedOrder = { ...order, payments: nextPayments }\n if (step.event) {\n next = {\n ...next,\n timeline: CommerceModel.appendOrderEvent(\n order,\n step.event.event,\n step.event.detail,\n nowMs,\n ),\n }\n }\n const completion = posCompletionPatch(next, nextPayments, nowMs)\n const write: Record<string, unknown> = {\n payments: nextPayments,\n ...(next.timeline !== order.timeline ? { timeline: next.timeline } : {}),\n }\n if (completion) {\n Object.assign(write, completion.patch)\n next = { ...next, ...(completion.patch as Partial<PosLiftedOrder>) }\n Object.assign(write, CommerceModel.orderListFields(next, orderRef.id))\n // ACCRUED IN THE SAME COMMIT AS THE FLIP (the AGL-2111 rule): the part\n // of the take no card payout carried goes on the org's invoice, and an\n // order and the fee it owes cannot diverge.\n const feeOrgId = String(order.posFeeOrgId ?? '')\n if (completion.invoiceTakeCents > 0 && feeOrgId) {\n const month = offlineFeeMonthKey(new Date(nowMs))\n transaction.set(\n firestore.collection('orgs').doc(feeOrgId).collection('offlineFees').doc(month),\n {\n month,\n feeCents: firebaseAdmin.firestore.FieldValue.increment(completion.invoiceTakeCents),\n orders: firebaseAdmin.firestore.FieldValue.increment(1),\n },\n { merge: true },\n )\n }\n } else if (\n order.status === 'paid' &&\n step.payment.status === 'succeeded' &&\n existing?.status !== 'succeeded'\n ) {\n // MONEY THAT ARRIVED AFTER THE SALE WAS ALREADY PAID: a QR page paid\n // after the cashier took another tender. It is recorded, never\n // dropped, and said on the order so the merchant can refund it.\n write['timeline'] = CommerceModel.appendOrderEvent(\n next,\n 'pos-overpaid',\n `${CommerceModel.describeOrderPayment(step.payment)} arrived after the ` +\n 'sale was already paid. Refund it from Stripe.',\n nowMs,\n )\n }\n transaction.set(orderRef, write, { merge: true })\n options.write?.(transaction, context, step.payment, existing)\n return {\n ok: true as const,\n order: next,\n payment: step.payment,\n completed: Boolean(completion),\n changed: true,\n }\n })\n if (outcome.ok && outcome.completed) {\n await completePosSale(options.hostId, outcome.order)\n }\n return outcome\n}\n\n/*==========================================\n * \"A REGISTER SALE COMPLETED\" — the one point every POS sale passes through\n * (AGL-3607), whichever way it was paid: the ledger's last payment\n * ({@link completePosSale}), a single-tender cash or room sale\n * (`pos-order.ts`), or a legacy QR sale paid through the `commerce-draft`\n * webhook branch. Peripherals (a cloud receipt printer, a cash drawer kick)\n * and anything else that must follow a sale subscribe here rather than\n * patching each path.\n *=========================================*/\n\nexport interface PosSaleCompletedEvent {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n /**\n * Rung while the register was offline and synced later (AGL-3625): the\n * customer has already left, so nothing at the counter should react now.\n */\n offline?: boolean\n}\n\nexport type PosSaleCompletedListener = (event: PosSaleCompletedEvent) => void | Promise<void>\n\nconst saleCompletedListeners = new Set<PosSaleCompletedListener>()\n\n/** Subscribes to completed register sales; returns the unsubscribe. */\nexport function onPosSaleCompleted(listener: PosSaleCompletedListener): () => void {\n saleCompletedListeners.add(listener)\n return () => saleCompletedListeners.delete(listener)\n}\n\n/**\n * Tells every listener a register sale completed. Awaited, and never throws:\n * the money is already taken, so a listener's failure is logged, not\n * returned to a cashier who would ring the sale again.\n */\nexport async function notifyPosSaleCompleted(event: PosSaleCompletedEvent): Promise<void> {\n for (const listener of [...saleCompletedListeners]) {\n try {\n await listener(event)\n } catch (error) {\n console.error('[pos-sale] sale-completed listener failed', event.orderId, error)\n }\n }\n}\n\n/** Reads the sale as it stands, for the register's status polls. */\nexport async function readPosSale(\n hostId: string,\n orderId: string,\n): Promise<PosLiftedOrder | null> {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n .get()\n if (!snapshot.exists) return null\n return liftOrder(snapshot.id, snapshot.data())\n}\n\n/**\n * Everything that follows a sale being paid, run once by the call whose\n * commit completed it. Each step is guarded on its own and none can undo the\n * sale: the money is taken, so a failure here is logged and said on the order\n * rather than thrown back at a cashier who would ring it again.\n */\nexport async function completePosSale(\n hostId: string,\n order: PosLiftedOrder,\n): Promise<void> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const orderRef = hostRef.collection('orders').doc(order.$id)\n const notes: Array<{ event: string; detail: string }> = []\n\n // The promotion's slot becomes a redemption only now: a sale that was\n // voided half-paid never counted against the merchant's cap (AGL-305).\n if (order.discountId && order.discountHoldKey) {\n const settled = await settlePromotionSlot({\n firestore,\n ref: hostRef.collection('discounts').doc(order.discountId),\n holdKey: order.discountHoldKey,\n label: `pos discount ${order.discountId}`,\n })\n if (settled === 'missing' || settled === 'error') {\n notes.push({\n event: 'redemption-orphaned',\n detail:\n `Discount ${order.discountId} was applied to this sale but its ` +\n 'redemption could not be counted against its limit.',\n })\n }\n }\n\n // Stock comes off exactly once, here, compounding across lines of one\n // product (AGL-1828) and atomically against other channels (AGL-2320).\n const products = new Map<string, CommerceModel.HostProduct>()\n for (const line of order.lineItems ?? []) {\n try {\n if (!products.has(line.productId)) {\n const snapshot = await hostRef.collection('products').doc(line.productId).get()\n if (!snapshot.exists) continue\n products.set(line.productId, CommerceModel.liftLegacyProduct(snapshot.data() as any))\n }\n const product = products.get(line.productId) as CommerceModel.HostProduct\n const variantId = line.variantId ?? product.variants[0]?.id\n const tracked = product.variants.some(\n (variant) => variant.id === variantId && variant.inventory != null,\n )\n if (!variantId || !tracked) continue\n const moved = await decrementVariantStock({\n firestore,\n hostRef,\n hostId,\n productId: line.productId,\n variantId,\n quantity: line.quantity,\n locationId: order.locationId || undefined,\n ledger: { reason: 'sale', orderId: order.$id },\n })\n if (!moved.before || !moved.after) continue\n products.set(line.productId, moved.after)\n alertLowStockCrossing(hostId, moved.before, moved.after)\n } catch (error) {\n console.error('[pos-sale] stock decrement failed', order.$id, line.productId, error)\n }\n }\n\n const receipt = order.receiptRequest\n const receiptEmail = receipt?.channel === 'email' ? String(receipt.to ?? '') : ''\n const contactEmail = receiptEmail || String(order.customerEmail ?? '')\n const marketingOptIn = Boolean((receipt as { marketingOptIn?: boolean } | undefined)?.marketingOptIn)\n const totalCents = Number(order.totals?.totalCents ?? 0)\n if (contactEmail) {\n await recordCapturedContact({\n orgId: '',\n hostId,\n identity: { email: contactEmail },\n surface: 'relationship',\n lifecycleFloor: 'customer',\n // The customer's own tick on the display, the same opt-in the online\n // checkout records (AGL-3608); an unticked box records nothing.\n ...(marketingOptIn ? { marketingConsent: true } : {}),\n purchaseCents: totalCents,\n interaction: {\n source: 'order',\n refId: order.$id,\n summary: `In-store purchase ($${(totalCents / 100).toFixed(2)})`,\n },\n }).catch((error: unknown) => {\n console.error('[pos-sale] contact capture failed', error)\n })\n }\n\n // The receipt and the paid event, through the same doors every sale uses\n // (AGL-3610, AGL-3611): the buyer-notification door claims each message\n // per channel, so a second completion path can never mail twice, and it\n // texts the receipt when the customer chose a text and a provider is on.\n if ((receipt?.channel === 'email' || receipt?.channel === 'sms') && receipt.to) {\n // The customer named where it goes: it goes there, whatever else the\n // order holds (AGL-3608).\n const sent = await deliverChosenPosReceipt(\n { hostId, orderId: order.$id },\n { channel: receipt.channel, to: receipt.to, orderEmail: String(order.customerEmail ?? '') },\n )\n if (sent !== 'sent') {\n notes.push({\n event: 'receipt-unsent',\n detail: `The ${receipt.channel === 'sms' ? 'text' : 'email'} receipt could not be sent.`,\n })\n }\n } else if (contactEmail) {\n await notifyOrderBuyer({ hostId, orderId: order.$id }, 'receipt', { email: contactEmail })\n }\n await raiseOrderEvent(ORDER_PAID_EVENT, { hostId, orderId: order.$id, key: 'paid' }).catch(\n (error: unknown) => console.error('[pos-sale] order.paid event failed', error),\n )\n\n if (notes.length) {\n await firestore\n .runTransaction(async (transaction: any) => {\n const fresh = liftOrder(orderRef.id, (await transaction.get(orderRef)).data())\n let timeline = fresh.timeline ?? []\n for (const note of notes) {\n timeline = CommerceModel.appendOrderEvent({ timeline }, note.event, note.detail)\n }\n transaction.set(orderRef, { timeline }, { merge: true })\n })\n .catch((error: unknown) => console.error('[pos-sale] note failed', error))\n }\n\n await notifyPosSaleCompleted({ hostId, orderId: order.$id, order })\n\n // The customer screen says thank you and forgets the customer: the state\n // is rewritten whole, so no address or phone outlives the sale (AGL-3608).\n if (order.registerId) {\n await resetPosDisplay(hostId, order.registerId).catch((error: unknown) =>\n console.error('[pos-sale] display reset failed', error),\n )\n }\n}\n\n/**\n * Sends a receipt the customer (or the cashier for them) explicitly asked\n * for, to the address or number they gave (AGL-3607, AGL-3608).\n *\n * It goes through the buyer-notification door first, so the order's\n * once-per-channel marker is claimed and a completion that races this call\n * cannot send a second copy. The door can skip it, though, for reasons that\n * do not apply to a request made at the counter: the store switched\n * automatic receipts or texts off, a receipt already went out on that\n * channel to an earlier address, or the door would use the email the order\n * was opened with rather than the one just typed. In each of those cases the\n * receipt is sent explicitly, as the order dialog's \"Resend receipt\" does.\n */\nexport async function deliverChosenPosReceipt(\n ref: { hostId: string; orderId: string },\n input: { channel: 'email' | 'sms'; to: string; orderEmail?: string },\n): Promise<'sent' | 'failed' | 'not_configured'> {\n const orderEmail = String(input.orderEmail ?? '').trim().toLowerCase()\n const to = input.to.trim()\n const doorAddressesIt = input.channel === 'sms' || !orderEmail || orderEmail === to.toLowerCase()\n if (doorAddressesIt) {\n const door = await notifyOrderBuyer(ref, 'receipt', input.channel === 'email' ? { email: to } : {})\n const entry =\n door.outcome === 'handled'\n ? door.channels.find((channel) => channel.channel === input.channel)\n : undefined\n if (entry?.outcome === 'sent') return 'sent'\n if (entry?.outcome === 'failed') return 'failed'\n // Already sent to this very address: nothing more to do.\n if (entry?.outcome === 'already' && input.channel === 'email') return 'sent'\n }\n const explicit = await sendOrderReceipt(ref, { channel: input.channel, to })\n if (explicit.outcome === 'sent') return 'sent'\n return explicit.outcome === 'not_configured' ? 'not_configured' : 'failed'\n}\n\n/**\n * Hands back a promotion slot an open sale was holding, when the sale is\n * voided rather than paid.\n */\nexport async function releasePosSaleDiscount(\n hostId: string,\n order: PosLiftedOrder,\n): Promise<void> {\n if (!order.discountId || !order.discountHoldKey) return\n await releasePromotionHold(\n firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('discounts')\n .doc(order.discountId),\n order.discountHoldKey,\n )\n}\n"],"names":["createHash","firebaseAdmin","recordCapturedContact","CommerceModel","alertLowStockCrossing","decrementVariantStock","releasePromotionHold","settlePromotionSlot","offlineFeeMonthKey","resetPosDisplay","notifyOrderBuyer","sendOrderReceipt","raiseOrderEvent","ORDER_PAID_EVENT","posPaymentId","orderId","attemptKey","update","digest","slice","posSaleSummary","order","payments","orderPayments","totalCents","Number","totals","$id","status","paidCents","posSettledCents","dueCents","posBalanceDueCents","tenderableCents","posTenderableCents","tipCents","posTipCents","map","payment","id","method","amountCents","cardBrand","last4","changeCents","checkoutUrl","readerId","creditLabel","failureMessage","livemode","posCompletionPatch","nowMs","takeFeeCents","Math","max","round","posTakeFeeCents","invoiceTakeCents","posInvoiceTakeCents","settled","filter","cardFeeCents","isCardPaymentMethod","reduce","sum","feeCents","intents","paymentIntentId","Boolean","tenders","describeOrderPayment","overpaid","patch","paidAtMs","feeCollection","posFeeCollection","length","paymentIntentIds","some","timeline","appendOrderEvent","join","toFixed","liftOrder","data","liftLegacyOrder","applyPosPayment","options","firestore","app","orderRef","collection","doc","hostId","Date","now","outcome","runTransaction","transaction","snapshot","get","exists","ok","error","channel","Array","isArray","context","prepare","undefined","existing","find","paymentId","step","decide","kind","completed","changed","nextPayments","next","event","detail","completion","write","Object","assign","orderListFields","feeOrgId","String","posFeeOrgId","month","set","FieldValue","increment","orders","merge","completePosSale","saleCompletedListeners","Set","onPosSaleCompleted","listener","add","delete","notifyPosSaleCompleted","console","readPosSale","receipt","hostRef","notes","discountId","discountHoldKey","ref","holdKey","label","push","products","Map","line","lineItems","product","has","productId","liftLegacyProduct","variantId","variants","tracked","variant","inventory","moved","quantity","locationId","ledger","reason","before","after","receiptRequest","receiptEmail","to","contactEmail","customerEmail","marketingOptIn","orgId","identity","email","surface","lifecycleFloor","marketingConsent","purchaseCents","interaction","source","refId","summary","catch","sent","deliverChosenPosReceipt","orderEmail","key","fresh","note","registerId","input","trim","toLowerCase","doorAddressesIt","door","entry","channels","explicit","releasePosSaleDiscount"],"mappings":"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"}
|
|
@@ -75,11 +75,37 @@ export declare function ensurePosTerminalLocation(options: {
|
|
|
75
75
|
}>;
|
|
76
76
|
/** The tipping a reader shows: Stripe offers three buttons, so three presets. */
|
|
77
77
|
export declare function posReaderTippingKey(settings: PosRegisterSettings): string;
|
|
78
|
+
type PosStripeParams = Record<string, string | number | boolean | undefined | null | readonly string[]>;
|
|
78
79
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* Writes `params` onto the site's ONE Terminal Configuration, creating it and
|
|
81
|
+
* assigning it to the site's Location on first use. Reader tipping and the
|
|
82
|
+
* start-up screen (AGL-4048) both live on it, because a Location takes one
|
|
83
|
+
* configuration override: a second configuration assigned for the screen
|
|
84
|
+
* would silently drop the tipping presets, and the other way round. Stripe
|
|
85
|
+
* leaves every parameter an update does not name as it was, so each caller
|
|
86
|
+
* sends only its own.
|
|
87
|
+
*
|
|
88
|
+
* The first write is the only race. Tipping's runs as a payment starts, the
|
|
89
|
+
* screen's when the POS devices card saves, and two at once would each make a
|
|
90
|
+
* configuration. So the new id is CLAIMED in a transaction: the loser writes
|
|
91
|
+
* its parameters onto the winner's configuration and deletes its own.
|
|
92
|
+
*
|
|
93
|
+
* `createKey` keys only the create (`pos-configuration:{hostId}:{createKey}`),
|
|
94
|
+
* so a retried first write makes one configuration. An update is not keyed:
|
|
95
|
+
* it sets values, and repeating it changes nothing.
|
|
96
|
+
*/
|
|
97
|
+
export declare function savePosReaderConfiguration(hostId: string, params: PosStripeParams, createKey: string): Promise<{
|
|
98
|
+
ok: true;
|
|
99
|
+
configurationId: string;
|
|
100
|
+
} | {
|
|
101
|
+
ok: false;
|
|
102
|
+
error: string;
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* Keeps the site's reader tipping in step with the merchant's settings, on
|
|
106
|
+
* the site's one Terminal Configuration ({@link savePosReaderConfiguration}),
|
|
107
|
+
* rewritten only when the presets changed. Never fatal: a reader that keeps
|
|
108
|
+
* the previous presets still takes the payment.
|
|
83
109
|
*/
|
|
84
110
|
export declare function syncPosReaderTipping(hostId: string, settings: PosRegisterSettings): Promise<void>;
|
|
85
111
|
/** Reads a reader from Stripe and keeps its status on the site's document. */
|
|
@@ -227,3 +253,40 @@ export declare function handlePosStripeEvent(input: {
|
|
|
227
253
|
hostId: string;
|
|
228
254
|
} | null>;
|
|
229
255
|
export type { PosLiftedOrder };
|
|
256
|
+
/** A PaymentIntent the offline sync reads, reduced to what it checks. */
|
|
257
|
+
export interface PosOfflineCardIntent {
|
|
258
|
+
id: string;
|
|
259
|
+
status: string;
|
|
260
|
+
amount: number;
|
|
261
|
+
currency: string;
|
|
262
|
+
metadata: Record<string, string>;
|
|
263
|
+
destination: string;
|
|
264
|
+
applicationFeeCents: number;
|
|
265
|
+
livemode: boolean;
|
|
266
|
+
brand?: string;
|
|
267
|
+
last4?: string;
|
|
268
|
+
failureMessage?: string;
|
|
269
|
+
}
|
|
270
|
+
/** Reads one offline card's PaymentIntent, with its charge's card details. */
|
|
271
|
+
export declare function retrievePosOfflineCardIntent(paymentIntentId: string): Promise<{
|
|
272
|
+
ok: true;
|
|
273
|
+
intent: PosOfflineCardIntent;
|
|
274
|
+
} | {
|
|
275
|
+
ok: false;
|
|
276
|
+
error: string;
|
|
277
|
+
}>;
|
|
278
|
+
/** Captures one authorized offline card with the platform's fee; a repeat finds the first capture. */
|
|
279
|
+
export declare function capturePosOfflineCardIntent(paymentIntentId: string, applicationFeeCents: number, idempotencyKey: string): Promise<{
|
|
280
|
+
ok: true;
|
|
281
|
+
intent: PosOfflineCardIntent;
|
|
282
|
+
} | {
|
|
283
|
+
ok: false;
|
|
284
|
+
error: string;
|
|
285
|
+
}>;
|
|
286
|
+
/**
|
|
287
|
+
* Turns Stripe's offline mode on for the site's readers: the same per-site
|
|
288
|
+
* Terminal Configuration the reader tipping uses, with `offline[enabled]`,
|
|
289
|
+
* assigned to the site's Location. Done once; a reader picks it up the next
|
|
290
|
+
* time it connects online. False when Stripe refused it.
|
|
291
|
+
*/
|
|
292
|
+
export declare function ensurePosOfflineCardConfiguration(hostId: string): Promise<boolean>;
|