@aglyn/plugins-commerce 1.0.0-beta.287 → 1.0.0-beta.289
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +29 -2
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
- package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +5 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
- package/src/lib/components/console/host-quotes-card.component.js +247 -0
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
- package/src/lib/components/console/order-webhooks-card.component.js +1 -1
- package/src/lib/components/console/order-webhooks-card.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +1266 -0
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
- package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
- package/src/lib/components/console/quote-pay-link.component.js +213 -0
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
- package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
- package/src/lib/components/console/quote-settings-card.component.js +121 -0
- package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
- package/src/lib/components/console/tax-settings-card.component.js +4 -1
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
- package/src/lib/components/quote-page-body.d.ts +33 -0
- package/src/lib/components/quote-page-body.js +696 -0
- package/src/lib/components/quote-page-body.js.map +1 -0
- package/src/lib/components/quote-page.d.ts +29 -0
- package/src/lib/components/quote-page.js +96 -0
- package/src/lib/components/quote-page.js.map +1 -0
- package/src/lib/components/storefront-payment-element.d.ts +7 -1
- package/src/lib/components/storefront-payment-element.js +8 -3
- package/src/lib/components/storefront-payment-element.js.map +1 -1
- package/src/lib/constants/orders-list-query.js +6 -1
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/quote-page.d.ts +30 -0
- package/src/lib/constants/quote-page.js +35 -0
- package/src/lib/constants/quote-page.js.map +1 -0
- package/src/lib/declarations.js +5 -1
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +25 -2
- package/src/lib/model/commerce-orders.js +10 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-quote-pay.d.ts +229 -0
- package/src/lib/model/commerce-quote-pay.js +393 -0
- package/src/lib/model/commerce-quote-pay.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +279 -0
- package/src/lib/model/commerce-quotes.js +522 -0
- package/src/lib/model/commerce-quotes.js.map +1 -0
- package/src/lib/model/index.d.ts +2 -0
- package/src/lib/model/index.js +2 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +8 -1
- package/src/lib/model/plugin-permissions.js +21 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/workspace-site-permissions.d.ts +26 -0
- package/src/lib/model/workspace-site-permissions.js +81 -0
- package/src/lib/model/workspace-site-permissions.js.map +1 -0
- package/src/lib/server/api-v1/orders-and-products.js +6 -2
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
- package/src/lib/server/billing-webhook.js +127 -52
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout-session-expiry.d.ts +36 -0
- package/src/lib/server/checkout-session-expiry.js +63 -0
- package/src/lib/server/checkout-session-expiry.js.map +1 -0
- package/src/lib/server/commerce-site-keys.d.ts +19 -0
- package/src/lib/server/commerce-site-keys.js +23 -2
- package/src/lib/server/commerce-site-keys.js.map +1 -1
- package/src/lib/server/draft-order.js +16 -8
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/order-webhooks.js +8 -8
- package/src/lib/server/order-webhooks.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +7 -0
- package/src/lib/server/person-eraser.js +24 -2
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-ops-gate.d.ts +13 -5
- package/src/lib/server/pos-ops-gate.js +12 -14
- package/src/lib/server/pos-ops-gate.js.map +1 -1
- package/src/lib/server/pos-refund-authority.d.ts +3 -1
- package/src/lib/server/pos-refund-authority.js +3 -2
- package/src/lib/server/pos-refund-authority.js.map +1 -1
- package/src/lib/server/pos-return.js +6 -7
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/pos-staff-pin.js +7 -7
- package/src/lib/server/pos-staff-pin.js.map +1 -1
- package/src/lib/server/quote-link-token.d.ts +39 -0
- package/src/lib/server/quote-link-token.js +97 -0
- package/src/lib/server/quote-link-token.js.map +1 -0
- package/src/lib/server/quote-pay.d.ts +83 -0
- package/src/lib/server/quote-pay.js +606 -0
- package/src/lib/server/quote-pay.js.map +1 -0
- package/src/lib/server/quote.d.ts +49 -0
- package/src/lib/server/quote.js +513 -0
- package/src/lib/server/quote.js.map +1 -0
- package/src/lib/server/refund.js +26 -36
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/related.js +2 -0
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/site-page-resolver.d.ts +2 -0
- package/src/lib/server/site-page-resolver.js +75 -0
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +6 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/transfer/tracking.server.js +6 -0
- package/src/lib/transfer/tracking.server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-ops-gate.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { PluginApiRequest } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost, getPluginConfig } from '@aglyn/tenant-data-admin'\nimport { findUserByUidAcrossPools } from '@aglyn/tenant-data-admin/server/auth-pools'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport {\n POS_CASHIER_ASSERTION_TTL_MS,\n POS_MANAGER_ASSERTION_TTL_MS,\n} from '../model/commerce-pos-ops'\nimport type { PrintReport } from '../model/commerce-printers'\nimport {\n holdsPosPermission,\n posPermissionRefusal,\n type PosPermission,\n} from '../model/plugin-permissions'\nimport { posOpsSettings, type PosOpsSettings } from '../pos-ops-config'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/*==========================================\n * WHO MAY RUN THE REGISTER (AGL-3609), for the shift, PIN, customer and\n * return routes.\n *\n * The fact the sale route checks: `managePos`, as resolved for THIS site —\n * the member's org role, custom role and overrides for an org-wide member,\n * and the role they hold here and their overrides for a site collaborator.\n * Plus the plan's `pos` entitlement, re-read every time because a downgrade\n * happens between taps.\n *\n * THE KEYS DECIDE, NOT THE SITE ROLE (AGL-4047). This also asked for `admin`\n * or `editor` in the site's `memberRoles`, exactly the set `managePos` is on\n * for by default, so nobody who had the register loses it; a site viewer the\n * workspace grants the register now works it.\n *\n * Every Firebase and Stripe touch goes through {@link PosOpsDeps}, so the\n * routes' specs run each money path against an in-memory store and a counted\n * fetch without mocking a module.\n *=========================================*/\n\nexport interface PosOpsMembership {\n orgWide: boolean\n hostRole: string | null\n /** `managePos`, and the register's finer keys (AGL-4047) as resolved. */\n permissions: { managePos?: boolean } & Partial<Record<PosPermission, boolean>>\n}\n\n/** The register's finer keys a membership carries (AGL-4047). */\nconst POS_PERMISSION_KEYS: readonly PosPermission[] = [\n 'posSell',\n 'posRefund',\n 'posDiscount',\n 'posCashDrawer',\n 'posDevices',\n 'posBuyHardware',\n]\n\nexport interface PosOpsDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string }>\n membership(uid: string, hostId: string): Promise<PosOpsMembership>\n orgForHost(hostId: string): Promise<{ orgId: string; org: Record<string, any> | null } | null>\n pluginConfig(orgId: string, hostId: string): Promise<Record<string, unknown>>\n /** The plan includes the register. */\n posEntitled(org: Record<string, any> | null): boolean\n /** The HMAC secret staff assertions are signed with; throws when unset. */\n signingSecret(): string\n /** A member's name as a receipt or a report prints it. */\n memberName(uid: string): Promise<string>\n now(): number\n /** Stripe's REST API; `global.fetch` in production. */\n fetch: typeof fetch\n /** The register's cloud printers (AGL-3619): drawer kicks and shift reports. */\n printer: PosOpsPrinter\n}\n\n/** What the register's routes ask of the printer queue. Never throws. */\nexport interface PosOpsPrinter {\n kickDrawer(input: {\n hostId: string\n registerId: string\n reason: 'paid_in' | 'paid_out' | 'drop' | 'cash_refund'\n causeId: string\n createdBy?: string\n }): Promise<{ jobIds: string[] }>\n printReport(input: {\n hostId: string\n registerId: string\n shiftId: string\n report: PrintReport\n createdBy?: string\n attemptKey: string\n }): Promise<{ jobIds: string[] }>\n}\n\nexport function defaultPosOpsDeps(): PosOpsDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid }\n },\n membership: async (uid, hostId) => {\n const resolved = await resolveOrgPermissions(uid, { hostId })\n return {\n orgWide: resolved.orgWide === true,\n hostRole: resolved.hostRole ?? null,\n permissions: {\n managePos: resolved.permissions?.managePos === true,\n // Each finer key as resolved; absent when the map carries none,\n // which `holdsPosPermission` reads as following `managePos`.\n ...Object.fromEntries(\n POS_PERMISSION_KEYS.filter(\n (key) => typeof resolved.permissions?.[key] === 'boolean',\n ).map((key) => [key, resolved.permissions[key] === true]),\n ),\n },\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n if (!found) return null\n return {\n orgId: String(found.org?.id ?? found.orgId ?? ''),\n org: (found.org as Record<string, any>) ?? null,\n }\n },\n pluginConfig: async (orgId, hostId) =>\n ((await getPluginConfig(orgId, 'commerce', { hostId })) ?? {}) as Record<string, unknown>,\n posEntitled: (org) => Aglyn.checkEntitlement(org as any, 'pos'),\n signingSecret: () => {\n const secret = process.env.TOKEN_SIGNING_SECRET\n if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured')\n return secret\n },\n memberName: async (uid) => {\n // Every pool: a member who signs in through SSO lives in a GCIP tenant\n // pool, not the project one (AGL-1122).\n try {\n const user = (await findUserByUidAcrossPools(uid))?.record\n return user?.displayName || user?.email || 'Staff member'\n } catch {\n return 'Staff member'\n }\n },\n now: () => Date.now(),\n fetch: (input, init) => fetch(input, init),\n // Loaded on use: the printing module reaches the whole sale pipeline,\n // which a shift count or a PIN check never needs.\n printer: {\n kickDrawer: async (input) => (await import('./pos-print')).kickPosDrawer(input),\n printReport: async (input) => (await import('./pos-print')).printPosShiftReport(input),\n },\n }\n}\n\n/** A JSON body, whether the dispatcher parsed it or not. */\nexport function posOpsBody(req: PluginApiRequest): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, any>) : {}\n}\n\n/** The request's idempotency key, either header spelling, bounded. */\nexport function posOpsIdempotencyKey(req: PluginApiRequest): string {\n return String(req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '')\n .trim()\n .slice(0, 200)\n}\n\n/** Who is at the register, once the gate admitted them. */\nexport interface PosOpsStaff {\n uid: string\n hostId: string\n orgId: string\n org: Record<string, any> | null\n membership: PosOpsMembership\n /** An admin of the whole workspace: the one role with no refund limit. */\n isManager: boolean\n settings: PosOpsSettings\n hostRef: FirebaseFirestore.DocumentReference\n}\n\nexport type PosOpsGate =\n | { ok: true; staff: PosOpsStaff }\n | { ok: false; status: number; error: string }\n\n/** A site id the dispatcher's own sentinels and path tricks cannot pass for. */\nfunction cleanId(value: unknown): string {\n const id = String(value ?? '').trim()\n return /^[A-Za-z0-9_-]{1,128}$/.test(id) && !/^__.*__$/.test(id) ? id : ''\n}\n\nexport { cleanId as posOpsCleanId }\n\n/**\n * Whether a member may work this site's register: `managePos`, as resolved\n * for the site (AGL-4047). A finer action also needs its own key:\n * `holdsPosPermission`.\n */\nexport function mayWorkRegister(membership: PosOpsMembership): boolean {\n return membership.permissions.managePos === true\n}\n\n/**\n * The one role whose say-so a refund above the cashier limit needs: an admin\n * of the whole workspace who is also this site's admin — the role\n * `refund.ts` has always required to refund at all.\n */\nexport function isPosManager(memberRole: unknown, membership: PosOpsMembership): boolean {\n return memberRole === 'admin' && membership.orgWide && membership.hostRole === 'admin'\n}\n\n/**\n * The register's gate for one route. `permission` names the finer register\n * key the action needs (AGL-4047) on top of working the register at all; a\n * member without it is answered 403 naming it.\n */\nexport async function authorizePosOps(\n deps: PosOpsDeps,\n req: PluginApiRequest,\n rawHostId: unknown,\n permission?: PosPermission,\n): Promise<PosOpsGate> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n const hostId = cleanId(rawHostId)\n if (!hostId) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await deps.verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const hostRef = deps.firestore().collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Still read: whether the member is a workspace admin and this site's\n // admin decides who may approve above the cashier's limits.\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[uid]\n const membership = await deps.membership(uid, hostId)\n if (!mayWorkRegister(membership)) {\n return { ok: false, status: 403, error: 'Not permitted' }\n }\n if (permission && !holdsPosPermission(membership.permissions, permission)) {\n return { ok: false, status: 403, error: posPermissionRefusal(permission) }\n }\n const owner = await deps.orgForHost(hostId)\n if (!deps.posEntitled(owner?.org ?? null)) {\n return { ok: false, status: 403, error: 'POS requires the Pro plan or above' }\n }\n const settings = posOpsSettings(\n owner?.orgId ? await deps.pluginConfig(owner.orgId, hostId).catch(() => ({})) : {},\n )\n return {\n ok: true,\n staff: {\n uid,\n hostId,\n orgId: owner?.orgId ?? '',\n org: owner?.org ?? null,\n membership,\n isManager: isPosManager(memberRole, membership),\n settings,\n hostRef,\n },\n }\n}\n\n/** A register on this site, or a refusal: a register id from another site is unknown here. */\nexport async function readPosRegister(\n staff: PosOpsStaff,\n rawRegisterId: unknown,\n): Promise<\n | { ok: true; ref: FirebaseFirestore.DocumentReference; data: Record<string, any> }\n | { ok: false; status: number; error: string }\n> {\n const registerId = cleanId(rawRegisterId)\n if (!registerId) return { ok: false, status: 400, error: 'Missing registerId' }\n const ref = staff.hostRef.collection('registers').doc(registerId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return { ok: false, status: 404, error: 'Unknown register' }\n return { ok: true, ref, data: (snapshot.data() ?? {}) as Record<string, any> }\n}\n\n/*==========================================\n * STAFF ASSERTIONS: what a PIN buys.\n *\n * A PIN never signs anybody in. It mints a short-lived, signed statement\n * that one member of THIS site is at THIS register — `{hostId, registerId,\n * memberUid, purpose, expiry}` under an HMAC — and the routes that take one\n * re-check that member's permissions on every use, so a PIN never\n * carries more than its member holds right now, and a revoked member's\n * assertion stops working on the next tap rather than at expiry.\n *=========================================*/\n\nexport type PosAssertionPurpose = 'cashier' | 'manager'\n\ninterface AssertionClaims {\n v: 1\n h: string\n r: string\n u: string\n p: PosAssertionPurpose\n e: number\n}\n\nfunction signClaims(deps: PosOpsDeps, payload: string): string {\n return createHmac('sha256', deps.signingSecret()).update(`pos-assertion:${payload}`).digest('hex')\n}\n\nexport function mintPosAssertion(\n deps: PosOpsDeps,\n input: { hostId: string; registerId: string; memberUid: string; purpose: PosAssertionPurpose },\n): { token: string; expiresAtMs: number } {\n const expiresAtMs =\n deps.now() +\n (input.purpose === 'manager' ? POS_MANAGER_ASSERTION_TTL_MS : POS_CASHIER_ASSERTION_TTL_MS)\n const claims: AssertionClaims = {\n v: 1,\n h: input.hostId,\n r: input.registerId,\n u: input.memberUid,\n p: input.purpose,\n e: expiresAtMs,\n }\n const payload = Buffer.from(JSON.stringify(claims)).toString('base64url')\n return { token: `${payload}.${signClaims(deps, payload)}`, expiresAtMs }\n}\n\n/**\n * The member an assertion names, or `null` — for a forged, expired, wrong-site,\n * wrong-register or wrong-purpose one alike. It does NOT re-check the member:\n * {@link resolvePosStaffAssertion} does that.\n */\nexport function readPosAssertion(\n deps: PosOpsDeps,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): { memberUid: string; expiresAtMs: number } | null {\n const value = typeof token === 'string' ? token.trim() : ''\n const dot = value.lastIndexOf('.')\n if (dot <= 0 || value.length > 2000) return null\n const payload = value.slice(0, dot)\n const signature = value.slice(dot + 1)\n let expected64: string\n try {\n expected64 = signClaims(deps, payload)\n } catch {\n return null\n }\n const a = Buffer.from(signature)\n const b = Buffer.from(expected64)\n if (a.length !== b.length || !timingSafeEqual(new Uint8Array(a), new Uint8Array(b))) return null\n let claims: AssertionClaims\n try {\n claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n if (\n claims?.v !== 1 ||\n claims.h !== expected.hostId ||\n claims.r !== expected.registerId ||\n claims.p !== expected.purpose ||\n typeof claims.u !== 'string' ||\n !claims.u ||\n !(Number(claims.e) > deps.now())\n ) {\n return null\n }\n return { memberUid: claims.u, expiresAtMs: Number(claims.e) }\n}\n\n/**\n * The member an assertion puts at the register, re-checked NOW against the\n * site's roles and the organization's permissions — or `null` when the\n * assertion is bad or the member may no longer do what it was minted for.\n */\nexport async function resolvePosStaffAssertion(\n deps: PosOpsDeps,\n hostRef: FirebaseFirestore.DocumentReference,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): Promise<string | null> {\n const read = readPosAssertion(deps, token, expected)\n if (!read) return null\n const membership = await deps.membership(read.memberUid, expected.hostId)\n if (expected.purpose !== 'manager') return mayWorkRegister(membership) ? read.memberUid : null\n const host = await hostRef.get()\n const memberRole = (host.get('memberRoles') ?? {})[read.memberUid]\n return isPosManager(memberRole, membership) ? read.memberUid : null\n}\n\n/**\n * Who rang this: the member a valid cashier assertion names, or the member\n * signed in on the device when there is none or it no longer holds. The\n * signed-in member already passed the gate, so the fallback never admits\n * anybody new; it only decides whose name the sale carries.\n */\nexport async function resolvePosCashier(\n deps: PosOpsDeps,\n staff: Pick<PosOpsStaff, 'uid' | 'hostId' | 'hostRef'>,\n registerId: string,\n token: unknown,\n): Promise<{ cashierId: string; via: 'pin' | 'session' }> {\n if (token) {\n const memberUid = await resolvePosStaffAssertion(deps, staff.hostRef, token, {\n hostId: staff.hostId,\n registerId,\n purpose: 'cashier',\n })\n if (memberUid) return { cashierId: memberUid, via: 'pin' }\n }\n return { cashierId: staff.uid, via: 'session' }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","getPluginConfig","findUserByUidAcrossPools","resolveOrgPermissions","createHmac","timingSafeEqual","POS_CASHIER_ASSERTION_TTL_MS","POS_MANAGER_ASSERTION_TTL_MS","holdsPosPermission","posPermissionRefusal","posOpsSettings","isRefusedIdToken","POS_PERMISSION_KEYS","defaultPosOpsDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","membership","hostId","resolved","orgWide","hostRole","permissions","managePos","Object","fromEntries","filter","key","map","orgForHost","found","orgId","String","org","id","pluginConfig","posEntitled","checkEntitlement","signingSecret","secret","process","env","TOKEN_SIGNING_SECRET","Error","memberName","user","record","displayName","email","now","Date","fetch","input","init","printer","kickDrawer","kickPosDrawer","printReport","printPosShiftReport","posOpsBody","req","body","parsed","JSON","parse","posOpsIdempotencyKey","headers","trim","slice","cleanId","value","test","posOpsCleanId","mayWorkRegister","isPosManager","memberRole","authorizePosOps","deps","rawHostId","permission","hostSnapshot","authorization","idToken","startsWith","length","ok","status","error","hostRef","collection","doc","get","exists","owner","settings","catch","staff","isManager","readPosRegister","rawRegisterId","snapshot","registerId","ref","data","signClaims","payload","update","digest","mintPosAssertion","expiresAtMs","purpose","claims","v","h","r","u","memberUid","p","e","Buffer","from","stringify","toString","readPosAssertion","expected","dot","lastIndexOf","signature","expected64","a","b","Uint8Array","Number","resolvePosStaffAssertion","host","read","resolvePosCashier","cashierId","via"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-ops-gate.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { PluginApiRequest } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost, getPluginConfig } from '@aglyn/tenant-data-admin'\nimport { findUserByUidAcrossPools } from '@aglyn/tenant-data-admin/server/auth-pools'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport {\n POS_CASHIER_ASSERTION_TTL_MS,\n POS_MANAGER_ASSERTION_TTL_MS,\n} from '../model/commerce-pos-ops'\nimport type { PrintReport } from '../model/commerce-printers'\nimport {\n holdsPosPermission,\n posPermissionRefusal,\n type PosPermission,\n} from '../model/plugin-permissions'\nimport { posOpsSettings, type PosOpsSettings } from '../pos-ops-config'\nimport { holdsCommerceWorkspaceSiteKey } from './commerce-site-keys'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/*==========================================\n * WHO MAY RUN THE REGISTER (AGL-3609), for the shift, PIN, customer and\n * return routes.\n *\n * The fact the sale route checks: `managePos`, as resolved for THIS site —\n * the member's org role, custom role and overrides for an org-wide member,\n * and the role they hold here and their overrides for a site collaborator.\n * Plus the plan's `pos` entitlement, re-read every time because a downgrade\n * happens between taps.\n *\n * THE KEYS DECIDE, NOT THE SITE ROLE (AGL-4047). This also asked for `admin`\n * or `editor` in the site's `memberRoles`, exactly the set `managePos` is on\n * for by default, so nobody who had the register loses it; a site viewer the\n * workspace grants the register now works it.\n *\n * Every Firebase and Stripe touch goes through {@link PosOpsDeps}, so the\n * routes' specs run each money path against an in-memory store and a counted\n * fetch without mocking a module.\n *=========================================*/\n\nexport interface PosOpsMembership {\n orgWide: boolean\n hostRole: string | null\n /** `managePos`, and the register's finer keys (AGL-4047) as resolved. */\n permissions: { managePos?: boolean } & Partial<Record<PosPermission, boolean>>\n}\n\n/** The register's finer keys a membership carries (AGL-4047). */\nconst POS_PERMISSION_KEYS: readonly PosPermission[] = [\n 'posSell',\n 'posRefund',\n 'posDiscount',\n 'posCashDrawer',\n 'posDevices',\n 'posBuyHardware',\n]\n\nexport interface PosOpsDeps {\n firestore(): FirebaseFirestore.Firestore\n verifyIdToken(token: string): Promise<{ uid: string }>\n membership(uid: string, hostId: string): Promise<PosOpsMembership>\n orgForHost(hostId: string): Promise<{ orgId: string; org: Record<string, any> | null } | null>\n pluginConfig(orgId: string, hostId: string): Promise<Record<string, unknown>>\n /** The plan includes the register. */\n posEntitled(org: Record<string, any> | null): boolean\n /** The HMAC secret staff assertions are signed with; throws when unset. */\n signingSecret(): string\n /** A member's name as a receipt or a report prints it. */\n memberName(uid: string): Promise<string>\n now(): number\n /** Stripe's REST API; `global.fetch` in production. */\n fetch: typeof fetch\n /** The register's cloud printers (AGL-3619): drawer kicks and shift reports. */\n printer: PosOpsPrinter\n}\n\n/** What the register's routes ask of the printer queue. Never throws. */\nexport interface PosOpsPrinter {\n kickDrawer(input: {\n hostId: string\n registerId: string\n reason: 'paid_in' | 'paid_out' | 'drop' | 'cash_refund'\n causeId: string\n createdBy?: string\n }): Promise<{ jobIds: string[] }>\n printReport(input: {\n hostId: string\n registerId: string\n shiftId: string\n report: PrintReport\n createdBy?: string\n attemptKey: string\n }): Promise<{ jobIds: string[] }>\n}\n\nexport function defaultPosOpsDeps(): PosOpsDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore(),\n verifyIdToken: async (token) => {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(token)\n return { uid: decoded.uid }\n },\n membership: async (uid, hostId) => {\n const resolved = await resolveOrgPermissions(uid, { hostId })\n return {\n orgWide: resolved.orgWide === true,\n hostRole: resolved.hostRole ?? null,\n permissions: {\n managePos: resolved.permissions?.managePos === true,\n // Each finer key as resolved; absent when the map carries none,\n // which `holdsPosPermission` reads as following `managePos`.\n ...Object.fromEntries(\n POS_PERMISSION_KEYS.filter(\n (key) => typeof resolved.permissions?.[key] === 'boolean',\n ).map((key) => [key, resolved.permissions[key] === true]),\n ),\n },\n }\n },\n orgForHost: async (hostId) => {\n const found = await getOrgForHost(hostId)\n if (!found) return null\n return {\n orgId: String(found.org?.id ?? found.orgId ?? ''),\n org: (found.org as Record<string, any>) ?? null,\n }\n },\n pluginConfig: async (orgId, hostId) =>\n ((await getPluginConfig(orgId, 'commerce', { hostId })) ?? {}) as Record<string, unknown>,\n posEntitled: (org) => Aglyn.checkEntitlement(org as any, 'pos'),\n signingSecret: () => {\n const secret = process.env.TOKEN_SIGNING_SECRET\n if (!secret) throw new Error('TOKEN_SIGNING_SECRET is not configured')\n return secret\n },\n memberName: async (uid) => {\n // Every pool: a member who signs in through SSO lives in a GCIP tenant\n // pool, not the project one (AGL-1122).\n try {\n const user = (await findUserByUidAcrossPools(uid))?.record\n return user?.displayName || user?.email || 'Staff member'\n } catch {\n return 'Staff member'\n }\n },\n now: () => Date.now(),\n fetch: (input, init) => fetch(input, init),\n // Loaded on use: the printing module reaches the whole sale pipeline,\n // which a shift count or a PIN check never needs.\n printer: {\n kickDrawer: async (input) => (await import('./pos-print')).kickPosDrawer(input),\n printReport: async (input) => (await import('./pos-print')).printPosShiftReport(input),\n },\n }\n}\n\n/** A JSON body, whether the dispatcher parsed it or not. */\nexport function posOpsBody(req: PluginApiRequest): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, any>) : {}\n}\n\n/** The request's idempotency key, either header spelling, bounded. */\nexport function posOpsIdempotencyKey(req: PluginApiRequest): string {\n return String(req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '')\n .trim()\n .slice(0, 200)\n}\n\n/** Who is at the register, once the gate admitted them. */\nexport interface PosOpsStaff {\n uid: string\n hostId: string\n orgId: string\n org: Record<string, any> | null\n membership: PosOpsMembership\n /**\n * \"Approve refunds over the register's limit\" (`commerce.posApprovals`,\n * AGL-4047): no refund limit, a manager's approval, other members' PINs.\n */\n isManager: boolean\n settings: PosOpsSettings\n hostRef: FirebaseFirestore.DocumentReference\n}\n\nexport type PosOpsGate =\n | { ok: true; staff: PosOpsStaff }\n | { ok: false; status: number; error: string }\n\n/** A site id the dispatcher's own sentinels and path tricks cannot pass for. */\nfunction cleanId(value: unknown): string {\n const id = String(value ?? '').trim()\n return /^[A-Za-z0-9_-]{1,128}$/.test(id) && !/^__.*__$/.test(id) ? id : ''\n}\n\nexport { cleanId as posOpsCleanId }\n\n/**\n * Whether a member may work this site's register: `managePos`, as resolved\n * for the site (AGL-4047). A finer action also needs its own key:\n * `holdsPosPermission`.\n */\nexport function mayWorkRegister(membership: PosOpsMembership): boolean {\n return membership.permissions.managePos === true\n}\n\n/**\n * Whose say-so a refund above the cashier limit needs, and who sets other\n * members' PINs: \"Approve refunds over the register's limit\"\n * (`commerce.posApprovals`, AGL-4047). Its preset is the gate this asked\n * before: an admin of the whole workspace who is also this site's admin,\n * which someone added to just this site as its admin never was. `hostDoc` is\n * the site's document (its stamp and `memberRoles`).\n */\nexport function isPosManager(\n hostDoc: { get(field: string): unknown } | null | undefined,\n uid: string,\n membership: PosOpsMembership,\n): boolean {\n return holdsCommerceWorkspaceSiteKey(hostDoc, uid, 'commerce.posApprovals', membership)\n}\n\n/**\n * The register's gate for one route. `permission` names the finer register\n * key the action needs (AGL-4047) on top of working the register at all; a\n * member without it is answered 403 naming it.\n */\nexport async function authorizePosOps(\n deps: PosOpsDeps,\n req: PluginApiRequest,\n rawHostId: unknown,\n permission?: PosPermission,\n): Promise<PosOpsGate> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n const hostId = cleanId(rawHostId)\n if (!hostId) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await deps.verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const hostRef = deps.firestore().collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) return { ok: false, status: 404, error: 'Unknown site' }\n const membership = await deps.membership(uid, hostId)\n if (!mayWorkRegister(membership)) {\n return { ok: false, status: 403, error: 'Not permitted' }\n }\n if (permission && !holdsPosPermission(membership.permissions, permission)) {\n return { ok: false, status: 403, error: posPermissionRefusal(permission) }\n }\n const owner = await deps.orgForHost(hostId)\n if (!deps.posEntitled(owner?.org ?? null)) {\n return { ok: false, status: 403, error: 'POS requires the Pro plan or above' }\n }\n const settings = posOpsSettings(\n owner?.orgId ? await deps.pluginConfig(owner.orgId, hostId).catch(() => ({})) : {},\n )\n return {\n ok: true,\n staff: {\n uid,\n hostId,\n orgId: owner?.orgId ?? '',\n org: owner?.org ?? null,\n membership,\n isManager: isPosManager(hostSnapshot, uid, membership),\n settings,\n hostRef,\n },\n }\n}\n\n/** A register on this site, or a refusal: a register id from another site is unknown here. */\nexport async function readPosRegister(\n staff: PosOpsStaff,\n rawRegisterId: unknown,\n): Promise<\n | { ok: true; ref: FirebaseFirestore.DocumentReference; data: Record<string, any> }\n | { ok: false; status: number; error: string }\n> {\n const registerId = cleanId(rawRegisterId)\n if (!registerId) return { ok: false, status: 400, error: 'Missing registerId' }\n const ref = staff.hostRef.collection('registers').doc(registerId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return { ok: false, status: 404, error: 'Unknown register' }\n return { ok: true, ref, data: (snapshot.data() ?? {}) as Record<string, any> }\n}\n\n/*==========================================\n * STAFF ASSERTIONS: what a PIN buys.\n *\n * A PIN never signs anybody in. It mints a short-lived, signed statement\n * that one member of THIS site is at THIS register — `{hostId, registerId,\n * memberUid, purpose, expiry}` under an HMAC — and the routes that take one\n * re-check that member's permissions on every use, so a PIN never\n * carries more than its member holds right now, and a revoked member's\n * assertion stops working on the next tap rather than at expiry.\n *=========================================*/\n\nexport type PosAssertionPurpose = 'cashier' | 'manager'\n\ninterface AssertionClaims {\n v: 1\n h: string\n r: string\n u: string\n p: PosAssertionPurpose\n e: number\n}\n\nfunction signClaims(deps: PosOpsDeps, payload: string): string {\n return createHmac('sha256', deps.signingSecret()).update(`pos-assertion:${payload}`).digest('hex')\n}\n\nexport function mintPosAssertion(\n deps: PosOpsDeps,\n input: { hostId: string; registerId: string; memberUid: string; purpose: PosAssertionPurpose },\n): { token: string; expiresAtMs: number } {\n const expiresAtMs =\n deps.now() +\n (input.purpose === 'manager' ? POS_MANAGER_ASSERTION_TTL_MS : POS_CASHIER_ASSERTION_TTL_MS)\n const claims: AssertionClaims = {\n v: 1,\n h: input.hostId,\n r: input.registerId,\n u: input.memberUid,\n p: input.purpose,\n e: expiresAtMs,\n }\n const payload = Buffer.from(JSON.stringify(claims)).toString('base64url')\n return { token: `${payload}.${signClaims(deps, payload)}`, expiresAtMs }\n}\n\n/**\n * The member an assertion names, or `null` — for a forged, expired, wrong-site,\n * wrong-register or wrong-purpose one alike. It does NOT re-check the member:\n * {@link resolvePosStaffAssertion} does that.\n */\nexport function readPosAssertion(\n deps: PosOpsDeps,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): { memberUid: string; expiresAtMs: number } | null {\n const value = typeof token === 'string' ? token.trim() : ''\n const dot = value.lastIndexOf('.')\n if (dot <= 0 || value.length > 2000) return null\n const payload = value.slice(0, dot)\n const signature = value.slice(dot + 1)\n let expected64: string\n try {\n expected64 = signClaims(deps, payload)\n } catch {\n return null\n }\n const a = Buffer.from(signature)\n const b = Buffer.from(expected64)\n if (a.length !== b.length || !timingSafeEqual(new Uint8Array(a), new Uint8Array(b))) return null\n let claims: AssertionClaims\n try {\n claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))\n } catch {\n return null\n }\n if (\n claims?.v !== 1 ||\n claims.h !== expected.hostId ||\n claims.r !== expected.registerId ||\n claims.p !== expected.purpose ||\n typeof claims.u !== 'string' ||\n !claims.u ||\n !(Number(claims.e) > deps.now())\n ) {\n return null\n }\n return { memberUid: claims.u, expiresAtMs: Number(claims.e) }\n}\n\n/**\n * The member an assertion puts at the register, re-checked NOW against the\n * site's roles and the organization's permissions — or `null` when the\n * assertion is bad or the member may no longer do what it was minted for.\n */\nexport async function resolvePosStaffAssertion(\n deps: PosOpsDeps,\n hostRef: FirebaseFirestore.DocumentReference,\n token: unknown,\n expected: { hostId: string; registerId: string; purpose: PosAssertionPurpose },\n): Promise<string | null> {\n const read = readPosAssertion(deps, token, expected)\n if (!read) return null\n const membership = await deps.membership(read.memberUid, expected.hostId)\n if (expected.purpose !== 'manager') return mayWorkRegister(membership) ? read.memberUid : null\n return isPosManager(await hostRef.get(), read.memberUid, membership) ? read.memberUid : null\n}\n\n/**\n * Who rang this: the member a valid cashier assertion names, or the member\n * signed in on the device when there is none or it no longer holds. The\n * signed-in member already passed the gate, so the fallback never admits\n * anybody new; it only decides whose name the sale carries.\n */\nexport async function resolvePosCashier(\n deps: PosOpsDeps,\n staff: Pick<PosOpsStaff, 'uid' | 'hostId' | 'hostRef'>,\n registerId: string,\n token: unknown,\n): Promise<{ cashierId: string; via: 'pin' | 'session' }> {\n if (token) {\n const memberUid = await resolvePosStaffAssertion(deps, staff.hostRef, token, {\n hostId: staff.hostId,\n registerId,\n purpose: 'cashier',\n })\n if (memberUid) return { cashierId: memberUid, via: 'pin' }\n }\n return { cashierId: staff.uid, via: 'session' }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","getPluginConfig","findUserByUidAcrossPools","resolveOrgPermissions","createHmac","timingSafeEqual","POS_CASHIER_ASSERTION_TTL_MS","POS_MANAGER_ASSERTION_TTL_MS","holdsPosPermission","posPermissionRefusal","posOpsSettings","holdsCommerceWorkspaceSiteKey","isRefusedIdToken","POS_PERMISSION_KEYS","defaultPosOpsDeps","firestore","app","verifyIdToken","token","decoded","auth","uid","membership","hostId","resolved","orgWide","hostRole","permissions","managePos","Object","fromEntries","filter","key","map","orgForHost","found","orgId","String","org","id","pluginConfig","posEntitled","checkEntitlement","signingSecret","secret","process","env","TOKEN_SIGNING_SECRET","Error","memberName","user","record","displayName","email","now","Date","fetch","input","init","printer","kickDrawer","kickPosDrawer","printReport","printPosShiftReport","posOpsBody","req","body","parsed","JSON","parse","posOpsIdempotencyKey","headers","trim","slice","cleanId","value","test","posOpsCleanId","mayWorkRegister","isPosManager","hostDoc","authorizePosOps","deps","rawHostId","permission","authorization","idToken","startsWith","length","ok","status","error","hostRef","collection","doc","hostSnapshot","get","exists","owner","settings","catch","staff","isManager","readPosRegister","rawRegisterId","snapshot","registerId","ref","data","signClaims","payload","update","digest","mintPosAssertion","expiresAtMs","purpose","claims","v","h","r","u","memberUid","p","e","Buffer","from","stringify","toString","readPosAssertion","expected","dot","lastIndexOf","signature","expected64","a","b","Uint8Array","Number","resolvePosStaffAssertion","read","resolvePosCashier","cashierId","via"],"mappings":"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"}
|
|
@@ -33,7 +33,7 @@ export declare function decidePosRefundAuthority(deps: PosOpsDeps, input: {
|
|
|
33
33
|
registerId: string;
|
|
34
34
|
refundCents: number;
|
|
35
35
|
limitCents: number;
|
|
36
|
-
/** The member asking
|
|
36
|
+
/** The member asking holds "Approve refunds over the register's limit" (AGL-4047). */
|
|
37
37
|
isManager: boolean;
|
|
38
38
|
managerAssertion: unknown;
|
|
39
39
|
}): Promise<PosRefundAuthority>;
|
|
@@ -57,4 +57,6 @@ export declare function posRegisterRefundAuthority(input: {
|
|
|
57
57
|
lineCents: number | null;
|
|
58
58
|
registerId: string;
|
|
59
59
|
managerAssertion: unknown;
|
|
60
|
+
/** The member asking holds "Approve refunds over the register's limit" themselves (AGL-4047). */
|
|
61
|
+
isManager?: boolean;
|
|
60
62
|
}, deps?: PosOpsDeps): Promise<PosRefundAuthority>;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { posMoney } from "../model/commerce-pos-ops.js";
|
|
17
|
+
import { commerceWorkspacePermissionRefusal } from "../model/workspace-site-permissions.js";
|
|
17
18
|
import { posOpsSettings } from "../pos-ops-config.js";
|
|
18
19
|
import { defaultPosOpsDeps, posOpsCleanId, resolvePosStaffAssertion } from "./pos-ops-gate.js";
|
|
19
20
|
/** The limit question, with the money already known. */ export async function decidePosRefundAuthority(deps, input) {
|
|
@@ -50,7 +51,7 @@ import { defaultPosOpsDeps, posOpsCleanId, resolvePosStaffAssertion } from "./po
|
|
|
50
51
|
return {
|
|
51
52
|
ok: false,
|
|
52
53
|
body: {
|
|
53
|
-
error: '
|
|
54
|
+
error: commerceWorkspacePermissionRefusal('commerce.refunds')
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
57
|
}
|
|
@@ -64,7 +65,7 @@ import { defaultPosOpsDeps, posOpsCleanId, resolvePosStaffAssertion } from "./po
|
|
|
64
65
|
registerId: input.registerId,
|
|
65
66
|
refundCents: Math.min(asked, remaining),
|
|
66
67
|
limitCents: settings.refundLimitCents,
|
|
67
|
-
isManager:
|
|
68
|
+
isManager: input.isManager === true,
|
|
68
69
|
managerAssertion: input.managerAssertion
|
|
69
70
|
});
|
|
70
71
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-refund-authority.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 { posMoney } from '../model/commerce-pos-ops'\nimport { posOpsSettings } from '../pos-ops-config'\nimport {\n defaultPosOpsDeps,\n posOpsCleanId,\n resolvePosStaffAssertion,\n type PosOpsDeps,\n} from './pos-ops-gate'\n\n/*==========================================\n * HOW MUCH THE REGISTER MAY REFUND (AGL-3609).\n *\n *
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-refund-authority.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 { posMoney } from '../model/commerce-pos-ops'\nimport { commerceWorkspacePermissionRefusal } from '../model/workspace-site-permissions'\nimport { posOpsSettings } from '../pos-ops-config'\nimport {\n defaultPosOpsDeps,\n posOpsCleanId,\n resolvePosStaffAssertion,\n type PosOpsDeps,\n} from './pos-ops-gate'\n\n/*==========================================\n * HOW MUCH THE REGISTER MAY REFUND (AGL-3609).\n *\n * \"Refund orders\" (`commerce.refunds`, AGL-4047) refunds anything, as a\n * workspace admin always could. A member who works the register refunds a\n * POS sale up to the site's cashier refund limit (`posRefundLimit`, $0 by\n * default, so out of the box every register refund needs a manager), and\n * above it only with \"Approve refunds over the register's limit\"\n * (`commerce.posApprovals`) or a MANAGER ASSERTION: the PIN of someone who\n * holds it, entered at that register for that refund, minted for two\n * minutes and re-checked against their permissions when it is used. Both\n * keys' preset is the gate they replaced: an admin of the whole workspace\n * who is also the site's admin.\n *\n * Only a POS sale. An online order's refund stays \"Refund orders\", so the\n * register's limit is not a way to refund the storefront.\n *=========================================*/\n\nexport type PosRefundAuthority =\n | { ok: true; approvedBy?: string }\n | { ok: false; body: { error: string; needsManager?: boolean; limitCents?: number } }\n\n/** The limit question, with the money already known. */\nexport async function decidePosRefundAuthority(\n deps: PosOpsDeps,\n input: {\n hostId: string\n hostRef: FirebaseFirestore.DocumentReference\n registerId: string\n refundCents: number\n limitCents: number\n /** The member asking holds \"Approve refunds over the register's limit\" (AGL-4047). */\n isManager: boolean\n managerAssertion: unknown\n },\n): Promise<PosRefundAuthority> {\n if (input.isManager || input.refundCents <= input.limitCents) return { ok: true }\n const registerId = posOpsCleanId(input.registerId)\n const approvedBy =\n registerId && input.managerAssertion\n ? await resolvePosStaffAssertion(deps, input.hostRef, input.managerAssertion, {\n hostId: input.hostId,\n registerId,\n purpose: 'manager',\n })\n : null\n if (approvedBy) return { ok: true, approvedBy }\n return {\n ok: false,\n body: {\n error:\n input.limitCents > 0\n ? `Refunds over ${posMoney(input.limitCents)} need a manager's PIN.`\n : \"Register refunds need a manager's PIN.\",\n needsManager: true,\n limitCents: input.limitCents,\n },\n }\n}\n\n/**\n * For `refund.ts`: a register member refunding from the order dialog. The\n * amount is the one the route is about to reserve — the amount asked, else\n * the named lines, else everything left.\n */\nexport async function posRegisterRefundAuthority(\n input: {\n hostId: string\n hostRef: FirebaseFirestore.DocumentReference\n order: {\n channel?: string\n totals?: { totalCents?: number } | null\n amountCents?: number\n refundedCents?: number\n }\n amountCents: number | null\n lineCents: number | null\n registerId: string\n managerAssertion: unknown\n /** The member asking holds \"Approve refunds over the register's limit\" themselves (AGL-4047). */\n isManager?: boolean\n },\n deps: PosOpsDeps = defaultPosOpsDeps(),\n): Promise<PosRefundAuthority> {\n if (input.order.channel !== 'pos') {\n return {\n ok: false,\n body: { error: commerceWorkspacePermissionRefusal('commerce.refunds') },\n }\n }\n const remaining = Math.max(\n 0,\n Math.round(Number(input.order.totals?.totalCents ?? input.order.amountCents ?? 0)) -\n Math.round(Number(input.order.refundedCents ?? 0)),\n )\n const asked =\n input.amountCents != null && Number.isFinite(input.amountCents)\n ? Math.round(input.amountCents)\n : input.lineCents != null\n ? input.lineCents\n : remaining\n const owner = await deps.orgForHost(input.hostId)\n const settings = posOpsSettings(\n owner?.orgId ? await deps.pluginConfig(owner.orgId, input.hostId).catch(() => ({})) : {},\n )\n return decidePosRefundAuthority(deps, {\n hostId: input.hostId,\n hostRef: input.hostRef,\n registerId: input.registerId,\n refundCents: Math.min(asked, remaining),\n limitCents: settings.refundLimitCents,\n isManager: input.isManager === true,\n managerAssertion: input.managerAssertion,\n })\n}\n"],"names":["posMoney","commerceWorkspacePermissionRefusal","posOpsSettings","defaultPosOpsDeps","posOpsCleanId","resolvePosStaffAssertion","decidePosRefundAuthority","deps","input","isManager","refundCents","limitCents","ok","registerId","approvedBy","managerAssertion","hostRef","hostId","purpose","body","error","needsManager","posRegisterRefundAuthority","order","channel","remaining","Math","max","round","Number","totals","totalCents","amountCents","refundedCents","asked","isFinite","lineCents","owner","orgForHost","settings","orgId","pluginConfig","catch","min","refundLimitCents"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,QAAQ,QAAQ,+BAA2B;AACpD,SAASC,kCAAkC,QAAQ,yCAAqC;AACxF,SAASC,cAAc,QAAQ,uBAAmB;AAClD,SACEC,iBAAiB,EACjBC,aAAa,EACbC,wBAAwB,QAEnB,oBAAgB;AAwBvB,sDAAsD,GACtD,OAAO,eAAeC,yBACpBC,IAAgB,EAChBC,KASC;IAED,IAAIA,MAAMC,SAAS,IAAID,MAAME,WAAW,IAAIF,MAAMG,UAAU,EAAE,OAAO;QAAEC,IAAI;IAAK;IAChF,MAAMC,aAAaT,cAAcI,MAAMK,UAAU;IACjD,MAAMC,aACJD,cAAcL,MAAMO,gBAAgB,GAChC,MAAMV,yBAAyBE,MAAMC,MAAMQ,OAAO,EAAER,MAAMO,gBAAgB,EAAE;QAC1EE,QAAQT,MAAMS,MAAM;QACpBJ;QACAK,SAAS;IACX,KACA;IACN,IAAIJ,YAAY,OAAO;QAAEF,IAAI;QAAME;IAAW;IAC9C,OAAO;QACLF,IAAI;QACJO,MAAM;YACJC,OACEZ,MAAMG,UAAU,GAAG,IACf,CAAC,aAAa,EAAEX,SAASQ,MAAMG,UAAU,EAAE,sBAAsB,CAAC,GAClE;YACNU,cAAc;YACdV,YAAYH,MAAMG,UAAU;QAC9B;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeW,2BACpBd,KAeC,EACDD,OAAmBJ,mBAAmB;QAUlBK,aACEA;QADFA;IARpB,IAAIA,MAAMe,KAAK,CAACC,OAAO,KAAK,OAAO;QACjC,OAAO;YACLZ,IAAI;YACJO,MAAM;gBAAEC,OAAOnB,mCAAmC;YAAoB;QACxE;IACF;IACA,MAAMwB,YAAYC,KAAKC,GAAG,CACxB,GACAD,KAAKE,KAAK,CAACC,QAAOrB,iBAAAA,sBAAAA,MAAMe,KAAK,CAACO,MAAM,qBAAlBtB,oBAAoBuB,UAAU,oBAAIvB,MAAMe,KAAK,CAACS,WAAW,YAAzDxB,OAA6D,MAC7EkB,KAAKE,KAAK,CAACC,QAAOrB,6BAAAA,MAAMe,KAAK,CAACU,aAAa,YAAzBzB,6BAA6B;IAEnD,MAAM0B,QACJ1B,MAAMwB,WAAW,IAAI,QAAQH,OAAOM,QAAQ,CAAC3B,MAAMwB,WAAW,IAC1DN,KAAKE,KAAK,CAACpB,MAAMwB,WAAW,IAC5BxB,MAAM4B,SAAS,IAAI,OACjB5B,MAAM4B,SAAS,GACfX;IACR,MAAMY,QAAQ,MAAM9B,KAAK+B,UAAU,CAAC9B,MAAMS,MAAM;IAChD,MAAMsB,WAAWrC,eACfmC,CAAAA,yBAAAA,MAAOG,KAAK,IAAG,MAAMjC,KAAKkC,YAAY,CAACJ,MAAMG,KAAK,EAAEhC,MAAMS,MAAM,EAAEyB,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA,KAAM,CAAC;IAEzF,OAAOpC,yBAAyBC,MAAM;QACpCU,QAAQT,MAAMS,MAAM;QACpBD,SAASR,MAAMQ,OAAO;QACtBH,YAAYL,MAAMK,UAAU;QAC5BH,aAAagB,KAAKiB,GAAG,CAACT,OAAOT;QAC7Bd,YAAY4B,SAASK,gBAAgB;QACrCnC,WAAWD,MAAMC,SAAS,KAAK;QAC/BM,kBAAkBP,MAAMO,gBAAgB;IAC1C;AACF"}
|
|
@@ -186,13 +186,15 @@ async function findOrders(hostRef, rawText) {
|
|
|
186
186
|
const digits = text.replace(/^#/, '');
|
|
187
187
|
if (/^\d{1,12}$/.test(digits)) {
|
|
188
188
|
const snapshot = await orders.where('number', '==', Number(digits)).limit(5).get();
|
|
189
|
+
// A quote takes an order number before it is paid (AGL-4093), and until
|
|
190
|
+
// then there is nothing to return.
|
|
189
191
|
return snapshot.docs.map((doc)=>{
|
|
190
192
|
var _doc_data;
|
|
191
193
|
return {
|
|
192
194
|
id: doc.id,
|
|
193
195
|
data: (_doc_data = doc.data()) != null ? _doc_data : {}
|
|
194
196
|
};
|
|
195
|
-
});
|
|
197
|
+
}).filter((order)=>!CommerceModel.isUnpaidQuote(order.data));
|
|
196
198
|
}
|
|
197
199
|
if (text.includes('@')) {
|
|
198
200
|
const snapshot = await orders.where('customerEmailLower', '==', nameSearchKey(text)).orderBy('createdAtMs', 'desc').limit(10).get();
|
|
@@ -340,7 +342,7 @@ export async function handlePosReturn(deps, req) {
|
|
|
340
342
|
};
|
|
341
343
|
const cashier = await resolvePosCashier(deps, staff, registerId, body['cashierAssertion']);
|
|
342
344
|
// The limit is the person at the till's: a PIN-switched cashier is held
|
|
343
|
-
// to it even on a device
|
|
345
|
+
// to it even on a device someone who holds the approval signed in on.
|
|
344
346
|
const cashierIsManager = cashier.cashierId === staff.uid ? staff.isManager : await memberIsPosManager(deps, staff.hostRef, staff.hostId, cashier.cashierId);
|
|
345
347
|
const authority = await decidePosRefundAuthority(deps, {
|
|
346
348
|
hostId: staff.hostId,
|
|
@@ -782,11 +784,8 @@ export async function handlePosReturn(deps, req) {
|
|
|
782
784
|
}
|
|
783
785
|
return held;
|
|
784
786
|
}
|
|
785
|
-
/** Whether a member
|
|
786
|
-
|
|
787
|
-
const host = await hostRef.get();
|
|
788
|
-
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
789
|
-
return isPosManager(role, await deps.membership(uid, hostId));
|
|
787
|
+
/** Whether a member holds "Approve refunds over the register's limit" here, asked now (AGL-4047). */ async function memberIsPosManager(deps, hostRef, hostId, uid) {
|
|
788
|
+
return isPosManager(await hostRef.get(), uid, await deps.membership(uid, hostId));
|
|
790
789
|
}
|
|
791
790
|
let ReturnRefusal = class ReturnRefusal extends Error {
|
|
792
791
|
constructor(status, message){
|