@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.291

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.
Files changed (140) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +2 -1
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  6. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  7. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  8. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  9. package/src/lib/components/console/host-orders-card.component.js +3 -2
  10. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  12. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  13. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  14. package/src/lib/components/console/invoice-send.component.js +237 -0
  15. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  16. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  17. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  18. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  19. package/src/lib/components/console/order-invoice.component.d.ts +15 -4
  20. package/src/lib/components/console/order-invoice.component.js +93 -12
  21. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  22. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  23. package/src/lib/components/console/quote-editor-dialog.component.js +154 -5
  24. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  25. package/src/lib/components/console/quote-proofs.component.d.ts +48 -0
  26. package/src/lib/components/console/quote-proofs.component.js +694 -0
  27. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  28. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  29. package/src/lib/components/console/quote-send.component.js +307 -0
  30. package/src/lib/components/console/quote-send.component.js.map +1 -0
  31. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  32. package/src/lib/components/console/quote-start.component.js +182 -0
  33. package/src/lib/components/console/quote-start.component.js.map +1 -0
  34. package/src/lib/components/product-options-field-body.d.ts +44 -0
  35. package/src/lib/components/product-options-field-body.js +260 -0
  36. package/src/lib/components/product-options-field-body.js.map +1 -0
  37. package/src/lib/components/product-options-field.d.ts +32 -0
  38. package/src/lib/components/product-options-field.js +128 -0
  39. package/src/lib/components/product-options-field.js.map +1 -0
  40. package/src/lib/components/quote-page-body.js +67 -20
  41. package/src/lib/components/quote-page-body.js.map +1 -1
  42. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  43. package/src/lib/components/quote-proof-panel.js +436 -0
  44. package/src/lib/components/quote-proof-panel.js.map +1 -0
  45. package/src/lib/model/commerce-invoices.d.ts +132 -0
  46. package/src/lib/model/commerce-invoices.js +332 -0
  47. package/src/lib/model/commerce-invoices.js.map +1 -0
  48. package/src/lib/model/commerce-orders.d.ts +29 -2
  49. package/src/lib/model/commerce-orders.js.map +1 -1
  50. package/src/lib/model/commerce-product-field.d.ts +23 -0
  51. package/src/lib/model/commerce-product-field.js +26 -0
  52. package/src/lib/model/commerce-product-field.js.map +1 -0
  53. package/src/lib/model/commerce-quote-email.d.ts +67 -0
  54. package/src/lib/model/commerce-quote-email.js +154 -0
  55. package/src/lib/model/commerce-quote-email.js.map +1 -0
  56. package/src/lib/model/commerce-quote-pay.d.ts +76 -6
  57. package/src/lib/model/commerce-quote-pay.js +105 -39
  58. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  59. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  60. package/src/lib/model/commerce-quote-proofs.js +348 -0
  61. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  62. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  63. package/src/lib/model/commerce-quote-request.js +207 -0
  64. package/src/lib/model/commerce-quote-request.js.map +1 -0
  65. package/src/lib/model/commerce-quotes.d.ts +140 -3
  66. package/src/lib/model/commerce-quotes.js.map +1 -1
  67. package/src/lib/model/index.d.ts +4 -0
  68. package/src/lib/model/index.js +4 -0
  69. package/src/lib/model/index.js.map +1 -1
  70. package/src/lib/plugin.js +19 -0
  71. package/src/lib/plugin.js.map +1 -1
  72. package/src/lib/server/billing-webhook.js +115 -70
  73. package/src/lib/server/billing-webhook.js.map +1 -1
  74. package/src/lib/server/commerce-pdf.d.ts +104 -0
  75. package/src/lib/server/commerce-pdf.js +310 -0
  76. package/src/lib/server/commerce-pdf.js.map +1 -0
  77. package/src/lib/server/draft-order.js +23 -3
  78. package/src/lib/server/draft-order.js.map +1 -1
  79. package/src/lib/server/invoice-email.d.ts +76 -0
  80. package/src/lib/server/invoice-email.js +183 -0
  81. package/src/lib/server/invoice-email.js.map +1 -0
  82. package/src/lib/server/order-invoice-send.d.ts +43 -0
  83. package/src/lib/server/order-invoice-send.js +218 -0
  84. package/src/lib/server/order-invoice-send.js.map +1 -0
  85. package/src/lib/server/order-print.d.ts +6 -0
  86. package/src/lib/server/order-print.js +22 -5
  87. package/src/lib/server/order-print.js.map +1 -1
  88. package/src/lib/server/order-status.d.ts +11 -0
  89. package/src/lib/server/order-status.js +44 -8
  90. package/src/lib/server/order-status.js.map +1 -1
  91. package/src/lib/server/pdf-response.d.ts +31 -0
  92. package/src/lib/server/pdf-response.js +30 -0
  93. package/src/lib/server/pdf-response.js.map +1 -0
  94. package/src/lib/server/person-eraser.d.ts +3 -2
  95. package/src/lib/server/person-eraser.js +25 -6
  96. package/src/lib/server/person-eraser.js.map +1 -1
  97. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  98. package/src/lib/server/pos-reader-splash.js +8 -93
  99. package/src/lib/server/pos-reader-splash.js.map +1 -1
  100. package/src/lib/server/quote-email.d.ts +85 -0
  101. package/src/lib/server/quote-email.js +211 -0
  102. package/src/lib/server/quote-email.js.map +1 -0
  103. package/src/lib/server/quote-link-token.d.ts +11 -0
  104. package/src/lib/server/quote-link-token.js +21 -0
  105. package/src/lib/server/quote-link-token.js.map +1 -1
  106. package/src/lib/server/quote-pay.d.ts +38 -6
  107. package/src/lib/server/quote-pay.js +115 -53
  108. package/src/lib/server/quote-pay.js.map +1 -1
  109. package/src/lib/server/quote-proofs.d.ts +51 -0
  110. package/src/lib/server/quote-proofs.js +500 -0
  111. package/src/lib/server/quote-proofs.js.map +1 -0
  112. package/src/lib/server/quote-uploads.d.ts +33 -0
  113. package/src/lib/server/quote-uploads.js +29 -0
  114. package/src/lib/server/quote-uploads.js.map +1 -0
  115. package/src/lib/server/quote.d.ts +25 -0
  116. package/src/lib/server/quote.js +638 -8
  117. package/src/lib/server/quote.js.map +1 -1
  118. package/src/lib/server/site-image-source.d.ts +38 -0
  119. package/src/lib/server/site-image-source.js +130 -0
  120. package/src/lib/server/site-image-source.js.map +1 -0
  121. package/src/lib/server/site-page-resolver.js +2 -1
  122. package/src/lib/server/site-page-resolver.js.map +1 -1
  123. package/src/lib/server.js +24 -1
  124. package/src/lib/server.js.map +1 -1
  125. package/src/lib/site.js +8 -0
  126. package/src/lib/site.js.map +1 -1
  127. package/src/lib/tenant-emails.js +243 -0
  128. package/src/lib/tenant-emails.js.map +1 -1
  129. package/src/lib/utils/commerce-document-pdf.d.ts +82 -0
  130. package/src/lib/utils/commerce-document-pdf.js +613 -0
  131. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  132. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  133. package/src/lib/utils/invoice-email-content.js +126 -0
  134. package/src/lib/utils/invoice-email-content.js.map +1 -0
  135. package/src/lib/utils/order-invoice.d.ts +52 -3
  136. package/src/lib/utils/order-invoice.js +124 -64
  137. package/src/lib/utils/order-invoice.js.map +1 -1
  138. package/src/lib/utils/quote-email-content.d.ts +62 -0
  139. package/src/lib/utils/quote-email-content.js +154 -0
  140. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -1 +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 type { PluginEventPayloads } from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport type { PluginJobHostGate } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgDoc, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { siteLockdownJobGate } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\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 * - when a logo is saved on the organization profile or the brand, for\n * every store of that workspace with a Location, once the save has\n * answered: core's `org.logo.changed` event ({@link onOrgLogoChanged});\n * - on the commerce plugin's `pos-reader-splash-refresh` beat, for every\n * store with a Location whose key went stale with nobody opening the\n * card: the backstop for an event that failed, and the only path for a\n * logo's file replaced in the media library ({@link refreshPosReaderSplashes}).\n *\n * Never on the payment path, and never IN FRONT of a logo's save: drawing\n * and uploading takes a second or two, and a reader with an older screen\n * 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\nexport function 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\nexport type 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/**\n * Draws each reader family's screen from the plan's source and uploads it to\n * Stripe: the image work, which needs `sharp`. Runs in the images function\n * on Vercel (`pos-reader-splash-images-route.ts`), and in any process off it.\n */\nexport async function drawAndUploadPosReaderSplash(\n hostId: string,\n source: SplashSource,\n): Promise<Record<PosReaderSplashFamilyId, string>> {\n const screens = await drawScreens(source)\n const fileIds = {} as Record<PosReaderSplashFamilyId, string>\n for (const family of POS_READER_SPLASH_FAMILIES) {\n fileIds[family.id] = await uploadSplashFile(hostId, family, screens[family.id])\n }\n return fileIds\n}\n\n/**\n * Each family's Stripe file for the plan (AGL-4048): drawn here where this\n * process can draw, else by the images function, which draws the plan with\n * the same key. A refusal meant for the store comes back as its own problem.\n */\nasync function posReaderSplashFileIds(\n hostId: string,\n key: string,\n source: SplashSource,\n): Promise<Record<PosReaderSplashFamilyId, string>> {\n const images = await import('./pos-reader-splash-images')\n const origin = images.posReaderSplashImagesOrigin()\n if (!origin) return drawAndUploadPosReaderSplash(hostId, source)\n try {\n const answer = await images.requestPosReaderSplashImages<{ fileIds: Record<PosReaderSplashFamilyId, string> }>(\n origin,\n { op: 'draw', hostId, key },\n )\n return answer.fileIds\n } catch (error) {\n if (error instanceof images.PosReaderSplashImagesRefusal) throw new PosReaderSplashRenderError(error.message)\n throw error\n }\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: {\n org?: Record<string, any> | null\n platform?: PlatformSplashDoc\n force?: boolean\n /**\n * Leave a screen that failed to apply alone until this long after the\n * failure, when it is still the same screen: a background pass must not\n * draw and upload a logo Stripe keeps refusing on every beat. The card\n * passes none, so opening it always tries again.\n */\n retryFailedAfterMs?: number\n } = {},\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 const failedAtMs = typeof config['splashFailedAtMs'] === 'number' ? (config['splashFailedAtMs'] as number) : 0\n if (\n options.retryFailedAfterMs &&\n !options.force &&\n config['splashFailedKey'] === plan.key &&\n Date.now() - failedAtMs < options.retryFailedAfterMs\n ) {\n const problem = typeof config['splashProblem'] === 'string' ? (config['splashProblem'] as string) : plan.problem\n return { outcome: 'skipped', plan, problem }\n }\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 fileIds = await posReaderSplashFileIds(hostId, plan.key, plan.source)\n for (const family of POS_READER_SPLASH_FAMILIES) {\n for (const deviceType of family.deviceTypes) params[`${deviceType}[splashscreen]`] = fileIds[family.id]\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\n .set({ splashProblem: problem, splashFailedAtMs: Date.now(), splashFailedKey: plan.key }, { merge: true })\n .catch(() => undefined)\n return { outcome: 'failed', plan, problem }\n }\n}\n\n/** How many stores one pass reaches, and how many it works on at once. */\nexport const POS_READER_SPLASH_SWEEP_WINDOW = 400\nconst POS_READER_SPLASH_SWEEP_CONCURRENCY = 4\n\nexport interface PosReaderSplashSweep {\n applied: number\n unchanged: number\n failed: number\n /** Locked stores, and screens still inside their failure's retry wait. */\n skipped: number\n /** More stores have readers than one pass reaches. */\n more: boolean\n}\n\n/**\n * Brings every store with a Location up to date with\n * {@link applyPosReaderSplash}, the call the POS devices card makes: a store\n * whose screen key has not moved costs reads and no Stripe call, and one\n * that moved gets exactly one round of uploads. At most\n * {@link POS_READER_SPLASH_SWEEP_WINDOW} stores a pass.\n */\nexport async function applyPosReaderSplashToStores(\n options: {\n platform?: PlatformSplashDoc\n /** Asked before each store; a locked store is left as it is. */\n isLocked?: (hostId: string) => Promise<boolean>\n retryFailedAfterMs?: number\n } = {},\n): Promise<PosReaderSplashSweep> {\n const counts: PosReaderSplashSweep = { applied: 0, unchanged: 0, failed: 0, skipped: 0, more: false }\n const platform = options.platform ?? (await readPlatformSplash())\n const snapshot = await firestore().collectionGroup('terminal').limit(POS_READER_SPLASH_SWEEP_WINDOW + 1).get()\n const docs = snapshot.docs as Array<{ id: string; ref: { path: string }; get: (field: string) => unknown }>\n const hostIds = docs\n .filter((doc) => doc.id === 'config' && doc.get('stripeLocationId'))\n .map((doc) => doc.ref.path.split('/')[1])\n .filter(Boolean)\n await applyPosReaderSplashQueue(hostIds.slice(0, POS_READER_SPLASH_SWEEP_WINDOW), counts, {\n ...options,\n platform,\n orgOf: ownerOrgOf,\n })\n return { ...counts, more: docs.length > POS_READER_SPLASH_SWEEP_WINDOW }\n}\n\n/** Works through `hostIds` a few at a time, counting each store's outcome into `counts`. */\nasync function applyPosReaderSplashQueue(\n hostIds: string[],\n counts: PosReaderSplashSweep,\n options: {\n platform: PlatformSplashDoc\n orgOf: (hostId: string) => Promise<Record<string, any> | null>\n isLocked?: (hostId: string) => Promise<boolean>\n retryFailedAfterMs?: number\n },\n): Promise<void> {\n const queue = [...hostIds]\n await Promise.all(\n Array.from({ length: POS_READER_SPLASH_SWEEP_CONCURRENCY }, async () => {\n for (let hostId = queue.shift(); hostId; hostId = queue.shift()) {\n if (options.isLocked && (await options.isLocked(hostId))) {\n counts.skipped += 1\n continue\n }\n const result = await applyPosReaderSplash(hostId, {\n org: await options.orgOf(hostId),\n platform: options.platform,\n ...(options.retryFailedAfterMs ? { retryFailedAfterMs: options.retryFailedAfterMs } : {}),\n })\n if (result.outcome === 'applied' || result.outcome === 'cleared') counts.applied += 1\n else if (result.outcome === 'failed') counts.failed += 1\n else if (result.outcome === 'skipped') counts.skipped += 1\n else counts.unchanged += 1\n }\n }),\n )\n}\n\n/** How long a background pass leaves a screen Stripe refused before trying it again. */\nexport const POS_READER_SPLASH_RETRY_FAILED_AFTER_MS = 6 * 60 * 60 * 1000\n\n/**\n * The `pos-reader-splash-refresh` beat (AGL-4048): a logo changed on the\n * organization profile or the white-label brand, or the logo's file replaced\n * in the media library, reaches the store's readers without anybody opening\n * the POS devices card. The screen's key already moves with each of those,\n * so the beat only has to ask; nothing waits on it, and a save never waits\n * on Stripe. Asks the runner's lockdown gate for every store.\n */\nexport async function refreshPosReaderSplashes(gate: {\n isLocked: (hostId: string) => Promise<boolean>\n}): Promise<PosReaderSplashSweep> {\n const idle: PosReaderSplashSweep = { applied: 0, unchanged: 0, failed: 0, skipped: 0, more: false }\n if (!posTerminalAvailable()) return idle\n return applyPosReaderSplashToStores({\n isLocked: (hostId) => gate.isLocked(hostId),\n retryFailedAfterMs: POS_READER_SPLASH_RETRY_FAILED_AFTER_MS,\n })\n}\n\n/**\n * Core's `org.logo.changed` (AGL-4048): a logo saved on the organization\n * profile or the white-label brand reaches the readers of every store of\n * that workspace now, not on the next beat. Raised after the save has\n * answered, so nobody waits on it.\n *\n * {@link applyPosReaderSplash} for each store, the call the POS devices card\n * makes: a store with no Location, or whose screen does not show the logo\n * that moved (a dark-mode logo, a store showing its own image), costs reads\n * and no Stripe call. A store that is locked is left alone, and a screen\n * Stripe refused waits as it does on the beat; the beat, which stays, is\n * the backstop for anything this misses. Never throws.\n */\nexport async function onOrgLogoChanged(\n payload: Pick<PluginEventPayloads['org.logo.changed'], 'orgId'>,\n gate: PluginJobHostGate = siteLockdownJobGate(),\n): Promise<PosReaderSplashSweep> {\n const counts: PosReaderSplashSweep = { applied: 0, unchanged: 0, failed: 0, skipped: 0, more: false }\n const orgId = String(payload.orgId ?? '').trim()\n if (!orgId || !posTerminalAvailable()) return counts\n try {\n const org = await getOrgDoc(orgId)\n if (!org) return counts\n const snapshot = await firestore()\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(POS_READER_SPLASH_SWEEP_WINDOW + 1)\n .get()\n const hostIds = (snapshot.docs as Array<{ id: string }>).map((doc) => String(doc.id))\n await applyPosReaderSplashQueue(hostIds.slice(0, POS_READER_SPLASH_SWEEP_WINDOW), counts, {\n platform: await readPlatformSplash(),\n orgOf: async () => ({ ...(org as Record<string, any>), id: orgId }),\n isLocked: (hostId) => gate.isLocked(hostId),\n retryFailedAfterMs: POS_READER_SPLASH_RETRY_FAILED_AFTER_MS,\n })\n return { ...counts, more: hostIds.length > POS_READER_SPLASH_SWEEP_WINDOW }\n } catch (error) {\n console.error('[pos-reader-splash] a changed logo could not reach the readers', orgId, error)\n return counts\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 action = String(body['action'] ?? '')\n // Changing what the readers show is setting them up (AGL-4047); reading it is not.\n const gate = await authorizePosStaff(req, hostId, action === 'save' ? 'posDevices' : undefined)\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 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","getOrgDoc","getOrgForHost","siteLockdownJobGate","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","drawAndUploadPosReaderSplash","fileIds","posReaderSplashFileIds","images","origin","posReaderSplashImagesOrigin","answer","requestPosReaderSplashImages","op","PosReaderSplashImagesRefusal","ownerOrgOf","owner","applyPosReaderSplash","options","outcome","plan","applied","force","failedAtMs","retryFailedAfterMs","Date","now","params","deviceType","deviceTypes","saved","set","splashAppliedKey","splashAppliedAtMs","splashProblem","FieldValue","delete","merge","console","splashFailedAtMs","splashFailedKey","POS_READER_SPLASH_SWEEP_WINDOW","POS_READER_SPLASH_SWEEP_CONCURRENCY","applyPosReaderSplashToStores","counts","unchanged","failed","skipped","more","collectionGroup","limit","docs","hostIds","filter","path","applyPosReaderSplashQueue","orgOf","queue","Promise","all","Array","shift","isLocked","result","POS_READER_SPLASH_RETRY_FAILED_AFTER_MS","refreshPosReaderSplashes","gate","idle","onOrgLogoChanged","payload","where","statusOf","available","logoUrl","locationReady","appliedAtMs","currentStatus","posReaderSplashHandler","req","res","status","action","staff","updatedAtMs","updatedBy","uid","includes","splash"],"mappings":"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"}
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 { formatMediaRef, resolveMediaSrc } from '@aglyn/aglyn/app-utils/media-ref'\nimport { checkEntitlement, resolveBrandingProfile } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport type { PluginEventPayloads } from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport type { PluginJobHostGate } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgDoc, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { siteLockdownJobGate } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\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'\nimport {\n locateImageSource,\n locateMediaImage,\n readImageSourceBytes,\n type LocatedImageSource,\n} from './site-image-source'\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 * - when a logo is saved on the organization profile or the brand, for\n * every store of that workspace with a Location, once the save has\n * answered: core's `org.logo.changed` event ({@link onOrgLogoChanged});\n * - on the commerce plugin's `pos-reader-splash-refresh` beat, for every\n * store with a Location whose key went stale with nobody opening the\n * card: the backstop for an event that failed, and the only path for a\n * logo's file replaced in the media library ({@link refreshPosReaderSplashes}).\n *\n * Never on the payment path, and never IN FRONT of a logo's save: drawing\n * and uploading takes a second or two, and a reader with an older screen\n * 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\nexport function 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: `site-image-source.ts`, with the reader screen's\n * size cap.\n *=========================================*/\n\nconst locateMediaAsset = (scopeSegment: string, mediaId: string, hostId: string, orgId: string) =>\n locateMediaImage(scopeSegment, mediaId, hostId, orgId, POS_READER_SPLASH_SOURCE_MAX_BYTES)\n\nconst locateSource = (value: string, hostId: string, orgId: string) =>\n locateImageSource(value, hostId, orgId, POS_READER_SPLASH_SOURCE_MAX_BYTES)\n\nconst readSourceBytes = (source: LocatedImageSource) => readImageSourceBytes(source, POS_READER_SPLASH_SOURCE_MAX_BYTES)\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\nexport type SplashSource =\n | { mode: 'platform'; doc: PlatformSplashDoc }\n | { mode: 'logo' | 'artwork'; located: LocatedImageSource }\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/**\n * Draws each reader family's screen from the plan's source and uploads it to\n * Stripe: the image work, which needs `sharp`. Runs in the images function\n * on Vercel (`pos-reader-splash-images-route.ts`), and in any process off it.\n */\nexport async function drawAndUploadPosReaderSplash(\n hostId: string,\n source: SplashSource,\n): Promise<Record<PosReaderSplashFamilyId, string>> {\n const screens = await drawScreens(source)\n const fileIds = {} as Record<PosReaderSplashFamilyId, string>\n for (const family of POS_READER_SPLASH_FAMILIES) {\n fileIds[family.id] = await uploadSplashFile(hostId, family, screens[family.id])\n }\n return fileIds\n}\n\n/**\n * Each family's Stripe file for the plan (AGL-4048): drawn here where this\n * process can draw, else by the images function, which draws the plan with\n * the same key. A refusal meant for the store comes back as its own problem.\n */\nasync function posReaderSplashFileIds(\n hostId: string,\n key: string,\n source: SplashSource,\n): Promise<Record<PosReaderSplashFamilyId, string>> {\n const images = await import('./pos-reader-splash-images')\n const origin = images.posReaderSplashImagesOrigin()\n if (!origin) return drawAndUploadPosReaderSplash(hostId, source)\n try {\n const answer = await images.requestPosReaderSplashImages<{ fileIds: Record<PosReaderSplashFamilyId, string> }>(\n origin,\n { op: 'draw', hostId, key },\n )\n return answer.fileIds\n } catch (error) {\n if (error instanceof images.PosReaderSplashImagesRefusal) throw new PosReaderSplashRenderError(error.message)\n throw error\n }\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: {\n org?: Record<string, any> | null\n platform?: PlatformSplashDoc\n force?: boolean\n /**\n * Leave a screen that failed to apply alone until this long after the\n * failure, when it is still the same screen: a background pass must not\n * draw and upload a logo Stripe keeps refusing on every beat. The card\n * passes none, so opening it always tries again.\n */\n retryFailedAfterMs?: number\n } = {},\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 const failedAtMs = typeof config['splashFailedAtMs'] === 'number' ? (config['splashFailedAtMs'] as number) : 0\n if (\n options.retryFailedAfterMs &&\n !options.force &&\n config['splashFailedKey'] === plan.key &&\n Date.now() - failedAtMs < options.retryFailedAfterMs\n ) {\n const problem = typeof config['splashProblem'] === 'string' ? (config['splashProblem'] as string) : plan.problem\n return { outcome: 'skipped', plan, problem }\n }\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 fileIds = await posReaderSplashFileIds(hostId, plan.key, plan.source)\n for (const family of POS_READER_SPLASH_FAMILIES) {\n for (const deviceType of family.deviceTypes) params[`${deviceType}[splashscreen]`] = fileIds[family.id]\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\n .set({ splashProblem: problem, splashFailedAtMs: Date.now(), splashFailedKey: plan.key }, { merge: true })\n .catch(() => undefined)\n return { outcome: 'failed', plan, problem }\n }\n}\n\n/** How many stores one pass reaches, and how many it works on at once. */\nexport const POS_READER_SPLASH_SWEEP_WINDOW = 400\nconst POS_READER_SPLASH_SWEEP_CONCURRENCY = 4\n\nexport interface PosReaderSplashSweep {\n applied: number\n unchanged: number\n failed: number\n /** Locked stores, and screens still inside their failure's retry wait. */\n skipped: number\n /** More stores have readers than one pass reaches. */\n more: boolean\n}\n\n/**\n * Brings every store with a Location up to date with\n * {@link applyPosReaderSplash}, the call the POS devices card makes: a store\n * whose screen key has not moved costs reads and no Stripe call, and one\n * that moved gets exactly one round of uploads. At most\n * {@link POS_READER_SPLASH_SWEEP_WINDOW} stores a pass.\n */\nexport async function applyPosReaderSplashToStores(\n options: {\n platform?: PlatformSplashDoc\n /** Asked before each store; a locked store is left as it is. */\n isLocked?: (hostId: string) => Promise<boolean>\n retryFailedAfterMs?: number\n } = {},\n): Promise<PosReaderSplashSweep> {\n const counts: PosReaderSplashSweep = { applied: 0, unchanged: 0, failed: 0, skipped: 0, more: false }\n const platform = options.platform ?? (await readPlatformSplash())\n const snapshot = await firestore().collectionGroup('terminal').limit(POS_READER_SPLASH_SWEEP_WINDOW + 1).get()\n const docs = snapshot.docs as Array<{ id: string; ref: { path: string }; get: (field: string) => unknown }>\n const hostIds = docs\n .filter((doc) => doc.id === 'config' && doc.get('stripeLocationId'))\n .map((doc) => doc.ref.path.split('/')[1])\n .filter(Boolean)\n await applyPosReaderSplashQueue(hostIds.slice(0, POS_READER_SPLASH_SWEEP_WINDOW), counts, {\n ...options,\n platform,\n orgOf: ownerOrgOf,\n })\n return { ...counts, more: docs.length > POS_READER_SPLASH_SWEEP_WINDOW }\n}\n\n/** Works through `hostIds` a few at a time, counting each store's outcome into `counts`. */\nasync function applyPosReaderSplashQueue(\n hostIds: string[],\n counts: PosReaderSplashSweep,\n options: {\n platform: PlatformSplashDoc\n orgOf: (hostId: string) => Promise<Record<string, any> | null>\n isLocked?: (hostId: string) => Promise<boolean>\n retryFailedAfterMs?: number\n },\n): Promise<void> {\n const queue = [...hostIds]\n await Promise.all(\n Array.from({ length: POS_READER_SPLASH_SWEEP_CONCURRENCY }, async () => {\n for (let hostId = queue.shift(); hostId; hostId = queue.shift()) {\n if (options.isLocked && (await options.isLocked(hostId))) {\n counts.skipped += 1\n continue\n }\n const result = await applyPosReaderSplash(hostId, {\n org: await options.orgOf(hostId),\n platform: options.platform,\n ...(options.retryFailedAfterMs ? { retryFailedAfterMs: options.retryFailedAfterMs } : {}),\n })\n if (result.outcome === 'applied' || result.outcome === 'cleared') counts.applied += 1\n else if (result.outcome === 'failed') counts.failed += 1\n else if (result.outcome === 'skipped') counts.skipped += 1\n else counts.unchanged += 1\n }\n }),\n )\n}\n\n/** How long a background pass leaves a screen Stripe refused before trying it again. */\nexport const POS_READER_SPLASH_RETRY_FAILED_AFTER_MS = 6 * 60 * 60 * 1000\n\n/**\n * The `pos-reader-splash-refresh` beat (AGL-4048): a logo changed on the\n * organization profile or the white-label brand, or the logo's file replaced\n * in the media library, reaches the store's readers without anybody opening\n * the POS devices card. The screen's key already moves with each of those,\n * so the beat only has to ask; nothing waits on it, and a save never waits\n * on Stripe. Asks the runner's lockdown gate for every store.\n */\nexport async function refreshPosReaderSplashes(gate: {\n isLocked: (hostId: string) => Promise<boolean>\n}): Promise<PosReaderSplashSweep> {\n const idle: PosReaderSplashSweep = { applied: 0, unchanged: 0, failed: 0, skipped: 0, more: false }\n if (!posTerminalAvailable()) return idle\n return applyPosReaderSplashToStores({\n isLocked: (hostId) => gate.isLocked(hostId),\n retryFailedAfterMs: POS_READER_SPLASH_RETRY_FAILED_AFTER_MS,\n })\n}\n\n/**\n * Core's `org.logo.changed` (AGL-4048): a logo saved on the organization\n * profile or the white-label brand reaches the readers of every store of\n * that workspace now, not on the next beat. Raised after the save has\n * answered, so nobody waits on it.\n *\n * {@link applyPosReaderSplash} for each store, the call the POS devices card\n * makes: a store with no Location, or whose screen does not show the logo\n * that moved (a dark-mode logo, a store showing its own image), costs reads\n * and no Stripe call. A store that is locked is left alone, and a screen\n * Stripe refused waits as it does on the beat; the beat, which stays, is\n * the backstop for anything this misses. Never throws.\n */\nexport async function onOrgLogoChanged(\n payload: Pick<PluginEventPayloads['org.logo.changed'], 'orgId'>,\n gate: PluginJobHostGate = siteLockdownJobGate(),\n): Promise<PosReaderSplashSweep> {\n const counts: PosReaderSplashSweep = { applied: 0, unchanged: 0, failed: 0, skipped: 0, more: false }\n const orgId = String(payload.orgId ?? '').trim()\n if (!orgId || !posTerminalAvailable()) return counts\n try {\n const org = await getOrgDoc(orgId)\n if (!org) return counts\n const snapshot = await firestore()\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(POS_READER_SPLASH_SWEEP_WINDOW + 1)\n .get()\n const hostIds = (snapshot.docs as Array<{ id: string }>).map((doc) => String(doc.id))\n await applyPosReaderSplashQueue(hostIds.slice(0, POS_READER_SPLASH_SWEEP_WINDOW), counts, {\n platform: await readPlatformSplash(),\n orgOf: async () => ({ ...(org as Record<string, any>), id: orgId }),\n isLocked: (hostId) => gate.isLocked(hostId),\n retryFailedAfterMs: POS_READER_SPLASH_RETRY_FAILED_AFTER_MS,\n })\n return { ...counts, more: hostIds.length > POS_READER_SPLASH_SWEEP_WINDOW }\n } catch (error) {\n console.error('[pos-reader-splash] a changed logo could not reach the readers', orgId, error)\n return counts\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 action = String(body['action'] ?? '')\n // Changing what the readers show is setting them up (AGL-4047); reading it is not.\n const gate = await authorizePosStaff(req, hostId, action === 'save' ? 'posDevices' : undefined)\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 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","formatMediaRef","resolveMediaSrc","checkEntitlement","resolveBrandingProfile","firebaseAdmin","getOrgDoc","getOrgForHost","siteLockdownJobGate","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","locateImageSource","locateMediaImage","readImageSourceBytes","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","locateSource","readSourceBytes","source","workspaceLogoValue","org","whiteLabel","brand","NO_LOGO","NO_IMAGE","planPosReaderSplash","config","platform","String","productName","setting","choice","effectiveChoice","logoValue","image","mediaScope","base","platformReady","key","problem","mode","located","version","slice","loadSharpFactory","loadSharp","drawScreens","sharp","screens","read","file","download","large","largeBody","own","body","contentType","bytes","facts","length","width","height","uploadSplashFile","screen","form","FormData","append","extension","Blob","Uint8Array","type","response","fetch","method","headers","Authorization","STRIPE_SECRET_KEY","json","catch","ok","error","message","drawAndUploadPosReaderSplash","fileIds","posReaderSplashFileIds","images","origin","posReaderSplashImagesOrigin","answer","requestPosReaderSplashImages","op","PosReaderSplashImagesRefusal","ownerOrgOf","owner","applyPosReaderSplash","options","outcome","plan","ref","applied","force","failedAtMs","retryFailedAfterMs","Date","now","params","deviceType","deviceTypes","saved","set","splashAppliedKey","splashAppliedAtMs","splashProblem","FieldValue","delete","merge","console","splashFailedAtMs","splashFailedKey","POS_READER_SPLASH_SWEEP_WINDOW","POS_READER_SPLASH_SWEEP_CONCURRENCY","applyPosReaderSplashToStores","counts","unchanged","failed","skipped","more","collectionGroup","limit","docs","hostIds","filter","path","split","applyPosReaderSplashQueue","orgOf","queue","Promise","all","Array","from","shift","isLocked","result","POS_READER_SPLASH_RETRY_FAILED_AFTER_MS","refreshPosReaderSplashes","gate","idle","onOrgLogoChanged","payload","where","statusOf","available","logoUrl","url","locationReady","appliedAtMs","currentStatus","posReaderSplashHandler","req","res","status","action","staff","updatedAtMs","updatedBy","uid","includes","splash"],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type EmailTheme, type RenderedHostEmail, type SendEmailOptions, type SendEmailResult, type SendingIdentityVerdict } from '@aglyn/shared-util-email';
18
+ import * as CommerceModel from '../model';
19
+ import type { quotePdfFile } from './commerce-pdf';
20
+ import type { CommercePdfFile } from './pdf-response';
21
+ /** What the route hands over for one send. */
22
+ export interface QuoteDeliveryInput {
23
+ hostId: string;
24
+ quoteId: string;
25
+ /** The quote as it stands, after any pay link replacement this send made. */
26
+ order: CommerceModel.HostOrder;
27
+ host: Record<string, unknown>;
28
+ store: Record<string, unknown> | null;
29
+ owner: {
30
+ id?: string;
31
+ org?: unknown;
32
+ } | null;
33
+ to: string;
34
+ /** The subject as typed; empty takes the default. */
35
+ subject: string;
36
+ message: string;
37
+ payUrl: string;
38
+ proofUrl: (proofId: string) => string | null;
39
+ linkReplaced: boolean;
40
+ /** The sending member's email: the Reply-To when the site has no contact email. */
41
+ senderEmail: string | null;
42
+ atMs: number;
43
+ }
44
+ export type QuoteDeliveryResult = {
45
+ sent: true;
46
+ messageId: string;
47
+ providerId: string | null;
48
+ subject: string;
49
+ replyTo: string | null;
50
+ thread: CommerceModel.QuoteEmailThread;
51
+ } | {
52
+ sent: false;
53
+ status: number;
54
+ error: string;
55
+ };
56
+ /** The calls the send makes, injectable for specs. */
57
+ export interface QuoteMailerDeps {
58
+ configured(): boolean;
59
+ sendingIdentity(hostId: string): Promise<SendingIdentityVerdict>;
60
+ render(hostId: string, key: string, tokens: Record<string, string>): Promise<RenderedHostEmail | null>;
61
+ /** The site's email theme, for the colors the composed proof markup draws with. */
62
+ theme(hostId: string): Promise<EmailTheme | null>;
63
+ /** The quote's PDF; loads the PDF writer and sharp only here. */
64
+ quotePdf(input: Parameters<typeof quotePdfFile>[0]): Promise<CommercePdfFile>;
65
+ send(options: SendEmailOptions): Promise<SendEmailResult>;
66
+ meter(hostId: string): Promise<void>;
67
+ /** The bounce and complaint list only: a quote is mail the customer asked for. */
68
+ suppression(email: string): Promise<'suppressed-platform' | null>;
69
+ }
70
+ export declare function defaultQuoteMailerDeps(): QuoteMailerDeps;
71
+ /** What the console's Send quote reaches: whether mail can go, whether this address may be mailed, and the send. */
72
+ export interface QuoteMailer {
73
+ configured(): boolean;
74
+ suppression(email: string): Promise<'suppressed-platform' | null>;
75
+ deliver(input: QuoteDeliveryInput): Promise<QuoteDeliveryResult>;
76
+ }
77
+ /** The business's name as its documents print it. */
78
+ export declare function quoteBusinessName(host: Record<string, unknown>): string;
79
+ /**
80
+ * Who a reply reaches: the site's contact email, else the person sending, as
81
+ * an inbox reply answers to them. Null only when neither is an address.
82
+ */
83
+ export declare function quoteReplyTo(host: Record<string, unknown>, senderEmail: string | null | undefined): string | null;
84
+ /** The quote email, said and sent. */
85
+ export declare function quoteMailer(deps?: QuoteMailerDeps): QuoteMailer;
@@ -0,0 +1,211 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { siteContactEmail, resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
18
+ import { resolveBrandingProfile } from "@aglyn/aglyn/app-utils/plan-entitlements";
19
+ import { emailThreadAfterSend, isEmailConfigured, mintEmailMessageId, nextThreadHeaders, PLATFORM_EMAIL_PALETTE, readEmailThread, resolveEmailColor, sendEmail } from "@aglyn/shared-util-email";
20
+ import { firebaseAdmin, hostSendingIdentity, meterHostEmail, renderHostEmailWithTokens, siteEmailFrame, transactionalAddressSuppression } from "@aglyn/tenant-data-admin";
21
+ import * as CommerceModel from "../model/index.js";
22
+ import { QUOTE_EMAIL_KEY, quoteEmailContent } from "../utils/quote-email-content.js";
23
+ export function defaultQuoteMailerDeps() {
24
+ const firestore = ()=>firebaseAdmin.app().firestore();
25
+ return {
26
+ configured: ()=>isEmailConfigured(),
27
+ sendingIdentity: (hostId)=>hostSendingIdentity(hostId),
28
+ render: (hostId, key, tokens)=>renderHostEmailWithTokens(firestore(), hostId, key, tokens),
29
+ theme: async (hostId)=>(await siteEmailFrame(firestore(), hostId, QUOTE_EMAIL_KEY)).theme,
30
+ quotePdf: async (input)=>(await import("./commerce-pdf.js")).quotePdfFile(input),
31
+ send: (options)=>sendEmail(options),
32
+ meter: async (hostId)=>{
33
+ await meterHostEmail(hostId);
34
+ },
35
+ suppression: (email)=>transactionalAddressSuppression(email)
36
+ };
37
+ }
38
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
39
+ /** The business's name as its documents print it. */ export function quoteBusinessName(host) {
40
+ var _host_name;
41
+ const named = resolveHostToken('businessName', host);
42
+ return String(named != null ? named : '').trim() || String((_host_name = host['name']) != null ? _host_name : '').trim();
43
+ }
44
+ /**
45
+ * Who a reply reaches: the site's contact email, else the person sending, as
46
+ * an inbox reply answers to them. Null only when neither is an address.
47
+ */ export function quoteReplyTo(host, senderEmail) {
48
+ const contact = siteContactEmail(host);
49
+ if (contact && EMAIL.test(contact)) return contact;
50
+ const sender = String(senderEmail != null ? senderEmail : '').trim();
51
+ return EMAIL.test(sender) ? sender : null;
52
+ }
53
+ /** The theme's colors for the composed proof markup, the platform's where the site's cannot answer. */ function proofColors(theme) {
54
+ const read = (token)=>{
55
+ var _ref, _resolveEmailColor;
56
+ return (_ref = (_resolveEmailColor = resolveEmailColor(token, theme == null ? void 0 : theme.palette)) != null ? _resolveEmailColor : resolveEmailColor(token, PLATFORM_EMAIL_PALETTE)) != null ? _ref : 'inherit';
57
+ };
58
+ return {
59
+ border: read('divider'),
60
+ muted: read('text.secondary')
61
+ };
62
+ }
63
+ /** A refused send in the words the console shows. */ function refusalOf(result) {
64
+ switch(result.reason){
65
+ case 'unconfigured':
66
+ return {
67
+ status: 503,
68
+ error: 'Email isn’t set up here, so the quote wasn’t sent.'
69
+ };
70
+ case 'rate-limited':
71
+ return {
72
+ status: 429,
73
+ error: 'Too many emails are going out right now. Try again in a minute.'
74
+ };
75
+ case 'suppressed':
76
+ case 'undeliverable':
77
+ return {
78
+ status: 409,
79
+ error: 'This address can’t receive email, so the quote wasn’t sent. Copy the pay link and send it another way.'
80
+ };
81
+ case 'unverified-domain':
82
+ return {
83
+ status: 409,
84
+ error: 'This site’s sending domain isn’t verified, so the quote wasn’t sent. Check Email settings.'
85
+ };
86
+ case 'suspended':
87
+ return {
88
+ status: 423,
89
+ error: 'This site can’t send email right now, so the quote wasn’t sent.'
90
+ };
91
+ case 'held-for-review':
92
+ return {
93
+ status: 409,
94
+ error: `The email was held for review, so it hasn’t gone out yet.${result.detail ? ` ${result.detail}` : ''}`
95
+ };
96
+ default:
97
+ return {
98
+ status: 502,
99
+ error: 'The quote email could not be sent. Try again.'
100
+ };
101
+ }
102
+ }
103
+ /** The quote email, said and sent. */ export function quoteMailer(deps = defaultQuoteMailerDeps()) {
104
+ return {
105
+ configured: ()=>deps.configured(),
106
+ suppression: (email)=>deps.suppression(email),
107
+ deliver: async (input)=>{
108
+ var _ref, _host_cname, _result_id;
109
+ var _input_store, _order_quote, _input_owner;
110
+ const { hostId, quoteId, order, host } = input;
111
+ const businessName = quoteBusinessName(host);
112
+ const subject = input.subject || CommerceModel.quoteEmailDefaultSubject(order, businessName, quoteId);
113
+ const [identity, theme] = await Promise.all([
114
+ deps.sendingIdentity(hostId),
115
+ deps.theme(hostId).catch(()=>null)
116
+ ]);
117
+ const content = quoteEmailContent({
118
+ order,
119
+ quoteId,
120
+ businessName,
121
+ currency: String((_ref = (_input_store = input.store) == null ? void 0 : _input_store['currency']) != null ? _ref : 'USD'),
122
+ subject,
123
+ message: input.message,
124
+ payUrl: input.payUrl,
125
+ proofUrl: input.proofUrl,
126
+ linkReplaced: input.linkReplaced,
127
+ colors: proofColors(theme),
128
+ radius: theme == null ? void 0 : theme.buttonRadius
129
+ });
130
+ // The PDF is what the email says is attached; without it nothing goes.
131
+ let pdf;
132
+ try {
133
+ pdf = await deps.quotePdf({
134
+ hostId,
135
+ quoteId,
136
+ order,
137
+ host,
138
+ store: input.store,
139
+ owner: input.owner,
140
+ payUrl: input.payUrl
141
+ });
142
+ } catch (error) {
143
+ console.error('[commerce/quote-email] the PDF could not be made', {
144
+ hostId,
145
+ quoteId,
146
+ error
147
+ });
148
+ return {
149
+ sent: false,
150
+ status: 500,
151
+ error: 'The quote’s PDF could not be made, so nothing was sent. Try again.'
152
+ };
153
+ }
154
+ const rendered = await deps.render(hostId, QUOTE_EMAIL_KEY, content.tokens);
155
+ const thread = readEmailThread((_order_quote = order.quote) == null ? void 0 : _order_quote.emailThread);
156
+ const siteHost = String((_host_cname = host['cname']) != null ? _host_cname : '').trim() || null;
157
+ const messageId = mintEmailMessageId([
158
+ identity == null ? void 0 : identity.domain,
159
+ identity == null ? void 0 : identity.from,
160
+ siteHost
161
+ ], {
162
+ label: 'quote',
163
+ atMs: input.atMs
164
+ });
165
+ const replyTo = quoteReplyTo(host, input.senderEmail);
166
+ const result = await deps.send(_extends({
167
+ to: input.to,
168
+ // The subject as sent, whatever a design's own says: it is what keeps
169
+ // the conversation together, and it was typed for this send.
170
+ subject,
171
+ text: (rendered == null ? void 0 : rendered.text) || content.text
172
+ }, (rendered == null ? void 0 : rendered.html) ? {
173
+ html: rendered.html
174
+ } : {}, {
175
+ headers: nextThreadHeaders(thread, messageId)
176
+ }, replyTo ? {
177
+ replyTo
178
+ } : {}, {
179
+ fromName: resolveBrandingProfile((_input_owner = input.owner) == null ? void 0 : _input_owner.org).fromName,
180
+ sendingIdentity: identity,
181
+ audience: 'tenant',
182
+ context: 'quote',
183
+ attachments: [
184
+ {
185
+ filename: pdf.filename,
186
+ contentType: 'application/pdf',
187
+ content: Buffer.from(pdf.bytes).toString('base64')
188
+ }
189
+ ]
190
+ }));
191
+ if (!result.sent) return _extends({
192
+ sent: false
193
+ }, refusalOf(result));
194
+ // Cost meter (AGL-1438), as every transactional site email.
195
+ await deps.meter(hostId).catch((error)=>console.error('[commerce/quote-email] not metered', error));
196
+ return {
197
+ sent: true,
198
+ messageId,
199
+ providerId: (_result_id = result.id) != null ? _result_id : null,
200
+ subject,
201
+ replyTo,
202
+ thread: emailThreadAfterSend(thread, {
203
+ messageId,
204
+ subject
205
+ })
206
+ };
207
+ }
208
+ };
209
+ }
210
+
211
+ //# sourceMappingURL=quote-email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/quote-email.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 { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { resolveBrandingProfile } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n emailThreadAfterSend,\n isEmailConfigured,\n mintEmailMessageId,\n nextThreadHeaders,\n PLATFORM_EMAIL_PALETTE,\n readEmailThread,\n resolveEmailColor,\n sendEmail,\n type EmailTheme,\n type RenderedHostEmail,\n type SendEmailOptions,\n type SendEmailResult,\n type SendingIdentityVerdict,\n} from '@aglyn/shared-util-email'\nimport {\n firebaseAdmin,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n siteEmailFrame,\n transactionalAddressSuppression,\n} from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { QUOTE_EMAIL_KEY, quoteEmailContent, type QuoteEmailColors } from '../utils/quote-email-content'\nimport type { quotePdfFile } from './commerce-pdf'\nimport type { CommercePdfFile } from './pdf-response'\n\n/*==========================================\n * SENDING THE QUOTE EMAIL (AGL-4097). Loaded only by the console's Send\n * quote (`POST commerce/quote { action: 'send' }`), so the renderer, the PDF\n * writer and sharp stay out of every other request.\n *\n * One email, the `quote-sent` catalog key in the site's theme and its header\n * and footer (or the site's own design of it), with the quote's PDF attached:\n *\n * - FROM the site's sending identity, under the site's name;\n * - REPLY-TO the site's contact email (its listing's, else its support\n * email), else the email of the person sending, so a reply reaches the\n * business either way, as an inbox reply's does;\n * - IN THE CUSTOMER'S CONVERSATION: a `Message-ID` minted on the sending\n * domain, and, when the quote already has a conversation (an earlier\n * send, or a request's confirmation), `In-Reply-To` and `References`\n * from it (`nextThreadHeaders`). The provider's own id is returned beside\n * ours, because whether a provider keeps a caller's `Message-ID` is its\n * own call, and a replaced one can then be traced;\n * - METERED as a site's transactional email;\n * - REFUSED only for an address that bounced for good or reported mail as\n * spam (`transactionalAddressSuppression`): the customer asked for the\n * quote, so an unsubscribe from the site's newsletter does not stop it,\n * as it does not stop a receipt (AGL-4098).\n *\n * The route decides everything about the quote (who may send, the link, the\n * stage); this module only says the email and sends it.\n *=========================================*/\n\n/** What the route hands over for one send. */\nexport interface QuoteDeliveryInput {\n hostId: string\n quoteId: string\n /** The quote as it stands, after any pay link replacement this send made. */\n order: CommerceModel.HostOrder\n host: Record<string, unknown>\n store: Record<string, unknown> | null\n owner: { id?: string; org?: unknown } | null\n to: string\n /** The subject as typed; empty takes the default. */\n subject: string\n message: string\n payUrl: string\n proofUrl: (proofId: string) => string | null\n linkReplaced: boolean\n /** The sending member's email: the Reply-To when the site has no contact email. */\n senderEmail: string | null\n atMs: number\n}\n\nexport type QuoteDeliveryResult =\n | {\n sent: true\n messageId: string\n providerId: string | null\n subject: string\n replyTo: string | null\n thread: CommerceModel.QuoteEmailThread\n }\n | { sent: false; status: number; error: string }\n\n/** The calls the send makes, injectable for specs. */\nexport interface QuoteMailerDeps {\n configured(): boolean\n sendingIdentity(hostId: string): Promise<SendingIdentityVerdict>\n render(hostId: string, key: string, tokens: Record<string, string>): Promise<RenderedHostEmail | null>\n /** The site's email theme, for the colors the composed proof markup draws with. */\n theme(hostId: string): Promise<EmailTheme | null>\n /** The quote's PDF; loads the PDF writer and sharp only here. */\n quotePdf(input: Parameters<typeof quotePdfFile>[0]): Promise<CommercePdfFile>\n send(options: SendEmailOptions): Promise<SendEmailResult>\n meter(hostId: string): Promise<void>\n /** The bounce and complaint list only: a quote is mail the customer asked for. */\n suppression(email: string): Promise<'suppressed-platform' | null>\n}\n\nexport function defaultQuoteMailerDeps(): QuoteMailerDeps {\n const firestore = () => firebaseAdmin.app().firestore()\n return {\n configured: () => isEmailConfigured(),\n sendingIdentity: (hostId) => hostSendingIdentity(hostId),\n render: (hostId, key, tokens) => renderHostEmailWithTokens(firestore(), hostId, key, tokens),\n theme: async (hostId) => (await siteEmailFrame(firestore(), hostId, QUOTE_EMAIL_KEY)).theme,\n quotePdf: async (input) => (await import('./commerce-pdf')).quotePdfFile(input),\n send: (options) => sendEmail(options),\n meter: async (hostId) => {\n await meterHostEmail(hostId)\n },\n suppression: (email) => transactionalAddressSuppression(email),\n }\n}\n\n/** What the console's Send quote reaches: whether mail can go, whether this address may be mailed, and the send. */\nexport interface QuoteMailer {\n configured(): boolean\n suppression(email: string): Promise<'suppressed-platform' | null>\n deliver(input: QuoteDeliveryInput): Promise<QuoteDeliveryResult>\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** The business's name as its documents print it. */\nexport function quoteBusinessName(host: Record<string, unknown>): string {\n const named = resolveHostToken('businessName', host as HostTokenSource)\n return String(named ?? '').trim() || String(host['name'] ?? '').trim()\n}\n\n/**\n * Who a reply reaches: the site's contact email, else the person sending, as\n * an inbox reply answers to them. Null only when neither is an address.\n */\nexport function quoteReplyTo(host: Record<string, unknown>, senderEmail: string | null | undefined): string | null {\n const contact = siteContactEmail(host as never)\n if (contact && EMAIL.test(contact)) return contact\n const sender = String(senderEmail ?? '').trim()\n return EMAIL.test(sender) ? sender : null\n}\n\n/** The theme's colors for the composed proof markup, the platform's where the site's cannot answer. */\nfunction proofColors(theme: EmailTheme | null): QuoteEmailColors {\n const read = (token: string) =>\n resolveEmailColor(token, theme?.palette) ?? resolveEmailColor(token, PLATFORM_EMAIL_PALETTE) ?? 'inherit'\n return { border: read('divider'), muted: read('text.secondary') }\n}\n\n/** A refused send in the words the console shows. */\nfunction refusalOf(result: Extract<SendEmailResult, { sent: false }>): { status: number; error: string } {\n switch (result.reason) {\n case 'unconfigured':\n return { status: 503, error: 'Email isn’t set up here, so the quote wasn’t sent.' }\n case 'rate-limited':\n return { status: 429, error: 'Too many emails are going out right now. Try again in a minute.' }\n case 'suppressed':\n case 'undeliverable':\n return { status: 409, error: 'This address can’t receive email, so the quote wasn’t sent. Copy the pay link and send it another way.' }\n case 'unverified-domain':\n return { status: 409, error: 'This site’s sending domain isn’t verified, so the quote wasn’t sent. Check Email settings.' }\n case 'suspended':\n return { status: 423, error: 'This site can’t send email right now, so the quote wasn’t sent.' }\n case 'held-for-review':\n return { status: 409, error: `The email was held for review, so it hasn’t gone out yet.${result.detail ? ` ${result.detail}` : ''}` }\n default:\n return { status: 502, error: 'The quote email could not be sent. Try again.' }\n }\n}\n\n/** The quote email, said and sent. */\nexport function quoteMailer(deps: QuoteMailerDeps = defaultQuoteMailerDeps()): QuoteMailer {\n return {\n configured: () => deps.configured(),\n suppression: (email) => deps.suppression(email),\n deliver: async (input) => {\n const { hostId, quoteId, order, host } = input\n const businessName = quoteBusinessName(host)\n const subject = input.subject || CommerceModel.quoteEmailDefaultSubject(order, businessName, quoteId)\n const [identity, theme] = await Promise.all([\n deps.sendingIdentity(hostId),\n deps.theme(hostId).catch(() => null),\n ])\n\n const content = quoteEmailContent({\n order,\n quoteId,\n businessName,\n currency: String(input.store?.['currency'] ?? 'USD'),\n subject,\n message: input.message,\n payUrl: input.payUrl,\n proofUrl: input.proofUrl,\n linkReplaced: input.linkReplaced,\n colors: proofColors(theme),\n radius: theme?.buttonRadius,\n })\n\n // The PDF is what the email says is attached; without it nothing goes.\n let pdf: CommercePdfFile\n try {\n pdf = await deps.quotePdf({\n hostId,\n quoteId,\n order,\n host,\n store: input.store,\n owner: input.owner,\n payUrl: input.payUrl,\n })\n } catch (error) {\n console.error('[commerce/quote-email] the PDF could not be made', { hostId, quoteId, error })\n return { sent: false, status: 500, error: 'The quote’s PDF could not be made, so nothing was sent. Try again.' }\n }\n\n const rendered = await deps.render(hostId, QUOTE_EMAIL_KEY, content.tokens)\n const thread = readEmailThread(order.quote?.emailThread)\n const siteHost = String(host['cname'] ?? '').trim() || null\n const messageId = mintEmailMessageId([identity?.domain, identity?.from, siteHost], {\n label: 'quote',\n atMs: input.atMs,\n })\n const replyTo = quoteReplyTo(host, input.senderEmail)\n\n const result = await deps.send({\n to: input.to,\n // The subject as sent, whatever a design's own says: it is what keeps\n // the conversation together, and it was typed for this send.\n subject,\n text: rendered?.text || content.text,\n ...(rendered?.html ? { html: rendered.html } : {}),\n headers: nextThreadHeaders(thread, messageId),\n ...(replyTo ? { replyTo } : {}),\n fromName: resolveBrandingProfile(input.owner?.org as never).fromName,\n sendingIdentity: identity,\n audience: 'tenant',\n context: 'quote',\n attachments: [\n {\n filename: pdf.filename,\n contentType: 'application/pdf',\n content: Buffer.from(pdf.bytes).toString('base64'),\n },\n ],\n })\n if (!result.sent) return { sent: false, ...refusalOf(result as Extract<SendEmailResult, { sent: false }>) }\n // Cost meter (AGL-1438), as every transactional site email.\n await deps.meter(hostId).catch((error) => console.error('[commerce/quote-email] not metered', error))\n return {\n sent: true,\n messageId,\n providerId: (result as Extract<SendEmailResult, { sent: true }>).id ?? null,\n subject,\n replyTo,\n thread: emailThreadAfterSend(thread, { messageId, subject }),\n }\n },\n }\n}\n"],"names":["siteContactEmail","resolveHostToken","resolveBrandingProfile","emailThreadAfterSend","isEmailConfigured","mintEmailMessageId","nextThreadHeaders","PLATFORM_EMAIL_PALETTE","readEmailThread","resolveEmailColor","sendEmail","firebaseAdmin","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","siteEmailFrame","transactionalAddressSuppression","CommerceModel","QUOTE_EMAIL_KEY","quoteEmailContent","defaultQuoteMailerDeps","firestore","app","configured","sendingIdentity","hostId","render","key","tokens","theme","quotePdf","input","quotePdfFile","send","options","meter","suppression","email","EMAIL","quoteBusinessName","host","named","String","trim","quoteReplyTo","senderEmail","contact","test","sender","proofColors","read","token","palette","border","muted","refusalOf","result","reason","status","error","detail","quoteMailer","deps","deliver","order","quoteId","businessName","subject","quoteEmailDefaultSubject","identity","Promise","all","catch","content","currency","store","message","payUrl","proofUrl","linkReplaced","colors","radius","buttonRadius","pdf","owner","console","sent","rendered","thread","quote","emailThread","siteHost","messageId","domain","from","label","atMs","replyTo","to","text","html","headers","fromName","org","audience","context","attachments","filename","contentType","Buffer","bytes","toString","providerId","id"],"mappings":"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"}