@aglyn/plugins-commerce 1.0.0-beta.221 → 1.0.0-beta.223
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 +4 -2
- package/package.json +14 -13
- package/src/lib/components/console/catalog-organization-card.component.js +9 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/discounts-card.component.js +8 -0
- package/src/lib/components/console/discounts-card.component.js.map +1 -1
- package/src/lib/components/console/gift-cards-card.component.js +8 -0
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/components/console/host-coupons-card.component.js +8 -0
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +34 -70
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +7 -6
- package/src/lib/components/console/product-zones.js +5 -4
- package/src/lib/components/console/product-zones.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +86 -284
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +67 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/model/commerce-gift-cards.d.ts +26 -0
- package/src/lib/model/commerce-gift-cards.js +42 -0
- package/src/lib/model/commerce-gift-cards.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +0 -57
- package/src/lib/model/commerce-orders.js +0 -80
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/index.d.ts +0 -1
- package/src/lib/model/index.js +0 -1
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +5 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +27 -0
- package/src/lib/server/api-v1/openapi.js +219 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
- package/src/lib/server/api-v1/orders-and-products.js +419 -0
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
- package/src/lib/server/billing-webhook.js +10 -13
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/download.js +4 -1
- package/src/lib/server/download.js.map +1 -1
- package/src/lib/server/fulfill-order.d.ts +52 -11
- package/src/lib/server/fulfill-order.js +6 -9
- package/src/lib/server/fulfill-order.js.map +1 -1
- package/src/lib/server/gift-card-issue.d.ts +57 -0
- package/src/lib/server/gift-card-issue.js +155 -0
- package/src/lib/server/gift-card-issue.js.map +1 -0
- package/src/lib/server/gift-cards.js +25 -84
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/membership-register.js +9 -8
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/newsletter.js +7 -5
- package/src/lib/server/newsletter.js.map +1 -1
- package/src/lib/server/paid-media-delivery.d.ts +21 -0
- package/src/lib/server/paid-media-delivery.js +23 -5
- package/src/lib/server/paid-media-delivery.js.map +1 -1
- package/src/lib/server/paid-media-serve-count.d.ts +28 -0
- package/src/lib/server/paid-media-serve-count.js +46 -0
- package/src/lib/server/paid-media-serve-count.js.map +1 -0
- package/src/lib/server/person-eraser.d.ts +45 -0
- package/src/lib/server/person-eraser.js +57 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/refund.js +15 -15
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/restock-flag.d.ts +2 -2
- package/src/lib/server/restock-flag.js +1 -1
- package/src/lib/server/restock-flag.js.map +1 -1
- package/src/lib/server/stream.js +4 -1
- package/src/lib/server/stream.js.map +1 -1
- package/src/lib/server.js +5 -21
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-person-records.d.ts +43 -0
- package/src/lib/testing/stand-in-person-records.js +63 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
- package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
- package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
- package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
- package/src/lib/transfer/gift-cards-plan.js +40 -0
- package/src/lib/transfer/gift-cards-plan.js.map +1 -0
- package/src/lib/transfer/gift-cards.server.d.ts +34 -0
- package/src/lib/transfer/gift-cards.server.js +296 -0
- package/src/lib/transfer/gift-cards.server.js.map +1 -0
- package/src/lib/transfer/list-filter.d.ts +29 -0
- package/src/lib/transfer/list-filter.js +112 -0
- package/src/lib/transfer/list-filter.js.map +1 -0
- package/src/lib/transfer/product-import-results.d.ts +29 -0
- package/src/lib/transfer/product-import-results.js +49 -0
- package/src/lib/transfer/product-import-results.js.map +1 -0
- package/src/lib/transfer/product-import-step.component.d.ts +30 -0
- package/src/lib/transfer/product-import-step.component.js +99 -0
- package/src/lib/transfer/product-import-step.component.js.map +1 -0
- package/src/lib/transfer/product-transfer.d.ts +211 -0
- package/src/lib/transfer/product-transfer.js +1594 -0
- package/src/lib/transfer/product-transfer.js.map +1 -0
- package/src/lib/transfer/products.server.d.ts +44 -0
- package/src/lib/transfer/products.server.js +550 -0
- package/src/lib/transfer/products.server.js.map +1 -0
- package/src/lib/transfer/records-transfer.d.ts +182 -0
- package/src/lib/transfer/records-transfer.js +1153 -0
- package/src/lib/transfer/records-transfer.js.map +1 -0
- package/src/lib/transfer/records.server.d.ts +68 -0
- package/src/lib/transfer/records.server.js +455 -0
- package/src/lib/transfer/records.server.js.map +1 -0
- package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
- package/src/lib/transfer/register-transfer-resources.js +171 -0
- package/src/lib/transfer/register-transfer-resources.js.map +1 -0
- package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
- package/src/lib/transfer/register-transfer-ui.js +78 -0
- package/src/lib/transfer/register-transfer-ui.js.map +1 -0
- package/src/lib/transfer/server-common.d.ts +76 -0
- package/src/lib/transfer/server-common.js +165 -0
- package/src/lib/transfer/server-common.js.map +1 -0
- package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
- package/src/lib/transfer/transfer-header-actions.component.js +79 -0
- package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
- package/src/lib/transfer/transfer-keys.d.ts +31 -0
- package/src/lib/transfer/transfer-keys.js +56 -0
- package/src/lib/transfer/transfer-keys.js.map +1 -0
- package/src/lib/model/commerce-io.d.ts +0 -36
- package/src/lib/model/commerce-io.js +0 -245
- package/src/lib/model/commerce-io.js.map +0 -1
- package/src/lib/server/contact-refund.d.ts +0 -117
- package/src/lib/server/contact-refund.js +0 -196
- package/src/lib/server/contact-refund.js.map +0 -1
|
@@ -89,7 +89,10 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
89
89
|
location: redirect.location,
|
|
90
90
|
expiresAtMs: redirect.expiresAtMs,
|
|
91
91
|
scope: asset.deliveryScope,
|
|
92
|
-
mediaId: asset.mediaId
|
|
92
|
+
mediaId: asset.mediaId,
|
|
93
|
+
collection: asset.collection,
|
|
94
|
+
scopeId: asset.scopeId,
|
|
95
|
+
sizeBytes: redirect.sizeBytes
|
|
93
96
|
};
|
|
94
97
|
};
|
|
95
98
|
const deliverAsset = async (scope, mediaId)=>{
|
|
@@ -174,6 +177,7 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
174
177
|
return deliverAsset(source.scope, source.mediaId);
|
|
175
178
|
case 'storage-object':
|
|
176
179
|
{
|
|
180
|
+
var _ref;
|
|
177
181
|
if (!io.bucketName || source.bucket !== io.bucketName) {
|
|
178
182
|
return refuse('foreign-storage');
|
|
179
183
|
}
|
|
@@ -197,14 +201,21 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
197
201
|
if (asset) return deliverAsset(asset.scope, asset.mediaId);
|
|
198
202
|
assertMediaSignatureTtl(ttlMs);
|
|
199
203
|
const expiresAtMs = nowMs + ttlMs;
|
|
204
|
+
const [location, sizeBytes] = await Promise.all([
|
|
205
|
+
io.signStorageRead(objectPath, expiresAtMs, {
|
|
206
|
+
attachment: storageReadIsAttachment(objectPath, options.cdnParams)
|
|
207
|
+
}),
|
|
208
|
+
(_ref = io.storageObjectSize == null ? void 0 : io.storageObjectSize.call(io, objectPath).catch(()=>0)) != null ? _ref : Promise.resolve(0)
|
|
209
|
+
]);
|
|
200
210
|
return {
|
|
201
211
|
ok: true,
|
|
202
212
|
via: 'signed-storage',
|
|
203
|
-
location
|
|
204
|
-
attachment: storageReadIsAttachment(objectPath, options.cdnParams)
|
|
205
|
-
}),
|
|
213
|
+
location,
|
|
206
214
|
expiresAtMs,
|
|
207
|
-
objectPath
|
|
215
|
+
objectPath,
|
|
216
|
+
collection: segments[0] === 'orgs' ? 'orgs' : 'hosts',
|
|
217
|
+
scopeId: segments[1],
|
|
218
|
+
sizeBytes
|
|
208
219
|
};
|
|
209
220
|
}
|
|
210
221
|
}
|
|
@@ -232,6 +243,13 @@ import { mediaCdnScopeRefusal, parseMediaCdnScope, selectAutoRendition } from "@
|
|
|
232
243
|
responseDisposition: 'attachment'
|
|
233
244
|
} : {}));
|
|
234
245
|
return String(url);
|
|
246
|
+
},
|
|
247
|
+
storageObjectSize: async (objectPath)=>{
|
|
248
|
+
const file = bucket.file(objectPath);
|
|
249
|
+
if (!file.getMetadata) return 0;
|
|
250
|
+
const [metadata] = await file.getMetadata();
|
|
251
|
+
const size = Number(metadata == null ? void 0 : metadata.size);
|
|
252
|
+
return Number.isSafeInteger(size) && size > 0 ? size : 0;
|
|
235
253
|
}
|
|
236
254
|
};
|
|
237
255
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/paid-media-delivery.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 {\n hostQualifiedScope,\n MEDIA_CDN_RENDITION_AUTO,\n MEDIA_CDN_RENDITION_PARAM,\n MEDIA_CDN_ROUTE,\n paidMediaAssetOfObject,\n parseMediaRenditions,\n parsePaidMediaSource,\n} from '@aglyn/aglyn/server'\nimport {\n type MediaDeliveryProvider,\n mediaDeliveryProvider,\n} from '@aglyn/aglyn/plugin-manager/media-delivery-provider'\nimport { lookalikeBrandForHost } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { mediaDeliveryRedirect } from '@aglyn/tenant-data-admin/server/media-delivery'\nimport {\n assertMediaSignatureTtl,\n mediaSignatureQuery,\n mintMediaSignature,\n} from '@aglyn/tenant-data-admin/server/media-signing'\nimport { isMediaStoragePathInScope } from '@aglyn/tenant-data-admin/server/media-storage-path'\nimport {\n mediaCdnScopeRefusal,\n parseMediaCdnScope,\n selectAutoRendition,\n} from '@aglyn/tenant-data-admin/server/serve-media-cdn'\n\n/**\n * Turning a URL stored on a product into a link a buyer may follow (AGL-2814).\n *\n * A product's paid media — its members videos — is stored as whatever the\n * author's picker wrote: a media reference, a CDN path, a raw Storage download\n * URL, or a hotlink somebody typed. `parsePaidMediaSource` says which of those\n * a value is. The commerce routes verify who is entitled and then redirect;\n * this module decides WHERE they may redirect, and its whole job is that the\n * answer is never a URL that outlives the purchase.\n *\n * ## The rules, in the order they are applied\n *\n * 1. **A library asset is delivered only when it is PRIVATE**, as a CDN URL\n * signed with {@link mintMediaSignature}. The CDN refuses a private asset on\n * every path without an unexpired signature, which is what makes the link\n * die. A public asset is refused here instead of redirected to: its URL\n * works for anyone forever, so handing it to a buyer is the leak itself.\n * 2. **A Storage download URL is never redirected to.** Its token lives as\n * long as the object. When the object belongs to a library asset, rule 1\n * applies to that asset. When no asset owns it (a legacy object, a derived\n * rendition file), it gets a V4 signed read URL with the same bounded\n * lifetime. When it is not in this platform's bucket, or not inside the\n * site's own library or its org's, it is refused.\n * 3. **Only the requesting site's own libraries are signed.** A product is\n * author data. Signing whatever scope it names would let one site's author\n * mint working links to another org's private files, so a host scope must\n * be this site and an org scope must be this site's org, and the signed\n * scope is always qualified with this site so the CDN's `visibleTo` check\n * runs against the site actually selling it.\n * 4. **A hotlink to somebody else's server passes through unchanged.** No code\n * of ours runs there and nothing here can make its URL expire. That is the\n * author's hosting decision, and the docs say so. EXCEPT a hotlink whose\n * host wears a brand it is not (`lookalikeBrandForHost`, the phishing\n * screen's rule): the redirect leaves from the merchant's own domain, so a\n * lure behind it is one no screen of the page or the receipt ever saw. It\n * is refused for every workspace, like a lookalike link in an email\n * (AGL-3363), and the caller files it.\n * 5. **A signed Storage read is a download when it is one.** A download, or\n * an object whose type a browser would run (HTML, SVG, XML), is signed\n * with `response-content-disposition: attachment`, so the file is saved\n * rather than rendered (AGL-3363). The CDN already does this for its own\n * deliveries; a V4 URL carried nothing.\n *\n * ## A video with a copy at the delivery provider (AGL-2824)\n *\n * A caller that asks for it with `delivery` gets, for a video that passes\n * rule 1, the delivery provider's own short-lived URL instead of the signed CDN\n * URL, whenever `mediaDeliveryRedirect` finds a current copy and the release\n * flag is on for the owning org. It is the URL the CDN would redirect the\n * signed request to anyway, minted one hop earlier and bounded by the same\n * lifetime. Anything else takes the signed CDN URL exactly as before.\n */\n\n/** Why a stored URL was not turned into a link. */\nexport type PaidMediaRefusal =\n /** A library asset the public can still fetch without a signature. */\n | 'not-private'\n /** No such asset, a deleted one, or one not shared with this site. */\n | 'not-found'\n /** Another site's library, another org's, or outside the media prefixes. */\n | 'out-of-scope'\n /** A Storage URL for a bucket this platform does not serve from. */\n | 'foreign-storage'\n /** Not a value any delivery can be built from. */\n | 'malformed'\n /** A hotlink to a host that wears another brand's name (AGL-3363). */\n | 'lookalike'\n\nexport type PaidMediaDelivery =\n | {\n ok: true\n via: 'signed-cdn'\n location: string\n expiresAtMs: number\n /** The scope segment the signature covers. */\n scope: string\n mediaId: string\n }\n | {\n ok: true\n via: 'signed-storage'\n location: string\n expiresAtMs: number\n objectPath: string\n }\n | { ok: true; via: 'external'; location: string }\n | {\n ok: true\n via: 'delivery'\n location: string\n expiresAtMs: number\n /** The scope segment the delivery was minted for. */\n scope: string\n mediaId: string\n }\n | {\n ok: false\n refusal: PaidMediaRefusal\n /** For a `lookalike`: the host it wears a brand in, for the caller to file. */\n host?: string\n }\n\n/** A document read, reduced to what this module asks of one. */\nexport interface PaidMediaDocument {\n exists: boolean\n get(field: string): unknown\n}\n\n/** Every read and signature this module needs, injectable for tests. */\nexport interface PaidMediaDeliveryIo {\n /** The platform's media bucket; empty when unconfigured. */\n bucketName: string\n readMedia(\n collection: 'hosts' | 'orgs',\n scopeId: string,\n mediaId: string,\n ): Promise<PaidMediaDocument>\n /** The org a site belongs to, or null for an unindexed site. */\n orgIdForHost(hostId: string): Promise<string | null>\n /**\n * A V4 signed read URL for one object in {@link bucketName}. `attachment`\n * signs `response-content-disposition: attachment` into it (AGL-3363).\n */\n signStorageRead(\n objectPath: string,\n expiresAtMs: number,\n options?: { attachment?: boolean },\n ): Promise<string>\n}\n\n/** Object types a browser runs rather than shows: always saved, never rendered. */\nconst ACTIVE_OBJECT_EXTENSION = /\\.(html?|xhtml|xht|svgz?|xml|xsl|xslt|mht|mhtml|js|mjs)$/i\n\n/** Is this signed read a download, by the caller's ask or by what the object is? */\nexport function storageReadIsAttachment(\n objectPath: string,\n cdnParams: ReadonlyArray<readonly [string, string]> | undefined,\n): boolean {\n if ((cdnParams ?? []).some(([key, value]) => key === 'download' && value === '1')) {\n return true\n }\n return ACTIVE_OBJECT_EXTENSION.test(objectPath)\n}\n\n/**\n * Resolves a stored product media URL to a redirect target that expires, or\n * says why there is none. See the module comment for the rules.\n *\n * `cdnParams` ride on a signed CDN URL ahead of the signature, which covers\n * `(scope, mediaId, exp)` and never the parameters, so they select a\n * representation of the one asset the signature names and nothing else.\n */\nexport async function resolvePaidMediaDelivery(options: {\n stored: unknown\n /** The site selling the media; the only libraries it may sign from. */\n hostId: string\n /** The lifetime of the link; bounded by `assertMediaSignatureTtl`. */\n ttlMs: number\n cdnParams?: ReadonlyArray<readonly [string, string]>\n /**\n * Hand a video with a current copy at the delivery provider its URL there\n * (AGL-2824). `accept` is the request's `Accept`, which chooses the\n * rendition when `cdnParams` asks for `?r=auto`. The provider and the flag\n * verdict default to the registered provider and the release flag.\n */\n delivery?: {\n accept?: unknown\n provider?: MediaDeliveryProvider | null\n enabled?: (orgId: string | null) => Promise<boolean>\n }\n nowMs?: number\n io: PaidMediaDeliveryIo\n}): Promise<PaidMediaDelivery> {\n const { hostId, ttlMs, io } = options\n const nowMs = options.nowMs ?? Date.now()\n const refuse = (refusal: PaidMediaRefusal): PaidMediaDelivery => ({\n ok: false,\n refusal,\n })\n if (!hostId) return refuse('out-of-scope')\n\n // One lookup per call, and only for the forms that need to know the org.\n let orgId: Promise<string | null> | undefined\n const siteOrgId = () => (orgId ??= io.orgIdForHost(hostId))\n\n /**\n * The delivery provider's URL for a private library video, or null for\n * the signed CDN URL. The representation is the one the CDN would pick for\n * the same `?r=` and `Accept`, so the buyer gets the copy the signed URL\n * would have been redirected to.\n */\n const deliverFromProvider = async (\n delivery: NonNullable<typeof options.delivery>,\n asset: {\n collection: 'hosts' | 'orgs'\n scopeId: string\n orgScope: boolean\n deliveryScope: string\n mediaId: string\n document: PaidMediaDocument\n },\n ): Promise<PaidMediaDelivery | null> => {\n const provider =\n delivery.provider === undefined ? mediaDeliveryProvider('deliver') : delivery.provider\n if (!provider) return null\n const requested = (options.cdnParams ?? []).find(\n ([key]) => key === MEDIA_CDN_RENDITION_PARAM,\n )?.[1]\n const renditions = parseMediaRenditions(asset.document.get('videoRenditions'))\n const rendition = !requested\n ? undefined\n : requested === MEDIA_CDN_RENDITION_AUTO\n ? selectAutoRendition(renditions, delivery.accept)\n : renditions.find((entry) => entry.key === requested)\n const redirect = await mediaDeliveryRedirect({\n provider,\n asset: {\n collection: asset.collection,\n scopeId: asset.scopeId,\n mediaId: asset.mediaId,\n },\n document: asset.document,\n rendition,\n servedType: rendition ? rendition.contentType : asset.document.get('contentType'),\n claims: { scope: asset.deliveryScope, mediaId: asset.mediaId, hostId },\n orgId: async () => (asset.orgScope ? asset.scopeId : await siteOrgId()),\n ...(delivery.enabled ? { deliveryEnabled: delivery.enabled } : {}),\n ttlMs,\n nowMs,\n }).catch(() => null)\n if (!redirect) return null\n return {\n ok: true,\n via: 'delivery',\n location: redirect.location,\n expiresAtMs: redirect.expiresAtMs,\n scope: asset.deliveryScope,\n mediaId: asset.mediaId,\n }\n }\n\n const deliverAsset = async (\n scope: string,\n mediaId: string,\n ): Promise<PaidMediaDelivery> => {\n const parsed = parseMediaCdnScope(scope)\n if (!parsed) return refuse('malformed')\n let collection: 'hosts' | 'orgs'\n let deliveryScope: string\n if (parsed.isOrg) {\n const owner = await siteOrgId()\n if (!owner || parsed.scopeId !== owner) return refuse('out-of-scope')\n collection = 'orgs'\n deliveryScope = hostQualifiedScope(scope, hostId)\n } else {\n if (parsed.scopeId !== hostId) return refuse('out-of-scope')\n collection = 'hosts'\n deliveryScope = hostId\n }\n const deliveryParsed = parseMediaCdnScope(deliveryScope)\n if (!deliveryParsed) return refuse('malformed')\n const document = await io.readMedia(collection, parsed.scopeId, mediaId)\n if (!document?.exists || document.get('deletedAt')) {\n return refuse('not-found')\n }\n // The CDN would 404 an org asset this site is not allowed to use, so a\n // link to one is a link that cannot play.\n if (mediaCdnScopeRefusal(deliveryParsed, document.get('visibleTo'))) {\n return refuse('not-found')\n }\n if (document.get('private') !== true) return refuse('not-private')\n assertMediaSignatureTtl(ttlMs)\n const delivered = options.delivery\n ? await deliverFromProvider(options.delivery, {\n collection,\n scopeId: parsed.scopeId,\n orgScope: parsed.isOrg,\n deliveryScope,\n mediaId,\n document,\n })\n : null\n if (delivered) return delivered\n const signature = mintMediaSignature(deliveryScope, mediaId, nowMs, ttlMs)\n const params = new URLSearchParams()\n for (const [key, value] of options.cdnParams ?? []) {\n if (key !== 'exp' && key !== 'sig') params.set(key, value)\n }\n const prefix = params.toString()\n return {\n ok: true,\n via: 'signed-cdn',\n location:\n `${MEDIA_CDN_ROUTE}/${deliveryScope}/${mediaId}?` +\n `${prefix ? `${prefix}&` : ''}${mediaSignatureQuery(signature)}`,\n expiresAtMs: signature.exp,\n scope: deliveryScope,\n mediaId,\n }\n }\n\n const source = parsePaidMediaSource(options.stored)\n switch (source.kind) {\n case 'malformed':\n return refuse('malformed')\n case 'external': {\n let externalHost: string\n try {\n externalHost = new URL(source.url).hostname\n } catch {\n return refuse('malformed')\n }\n if (lookalikeBrandForHost(externalHost)) {\n return { ok: false, refusal: 'lookalike', host: externalHost }\n }\n return { ok: true, via: 'external', location: source.url }\n }\n case 'asset':\n return deliverAsset(source.scope, source.mediaId)\n case 'storage-object': {\n if (!io.bucketName || source.bucket !== io.bucketName) {\n return refuse('foreign-storage')\n }\n const { objectPath } = source\n const segments = objectPath.split('/')\n let base: string | null = null\n if (segments[0] === 'hosts' && segments[1] === hostId) {\n base = `hosts/${hostId}`\n } else if (segments[0] === 'orgs' && segments[1]) {\n const owner = await siteOrgId()\n if (owner && segments[1] === owner) base = `orgs/${owner}`\n }\n // The same prefix test the CDN applies to a document's `storagePath`,\n // which also refuses traversal and empty segments.\n if (!base || !isMediaStoragePathInScope(objectPath, base)) {\n return refuse('out-of-scope')\n }\n // An object keyed like an asset IS that asset, and the asset's own rules\n // decide — including refusing it when the asset is still public.\n const asset = paidMediaAssetOfObject(objectPath)\n if (asset) return deliverAsset(asset.scope, asset.mediaId)\n assertMediaSignatureTtl(ttlMs)\n const expiresAtMs = nowMs + ttlMs\n return {\n ok: true,\n via: 'signed-storage',\n location: await io.signStorageRead(objectPath, expiresAtMs, {\n attachment: storageReadIsAttachment(objectPath, options.cdnParams),\n }),\n expiresAtMs,\n objectPath,\n }\n }\n }\n}\n\n/**\n * The production reads behind {@link resolvePaidMediaDelivery}, built on the\n * caller's Firestore and bucket handles so a route's own mocks reach them.\n */\nexport function createPaidMediaDeliveryIo(options: {\n firestore: {\n collection(name: string): {\n doc(id: string): {\n get(): Promise<PaidMediaDocument>\n collection(name: string): {\n doc(id: string): { get(): Promise<PaidMediaDocument> }\n }\n }\n }\n }\n bucket: {\n name?: string\n file(path: string): {\n getSignedUrl(config: {\n version: 'v4'\n action: 'read'\n expires: number\n responseDisposition?: string\n }): Promise<[string] | string[]>\n }\n }\n}): PaidMediaDeliveryIo {\n const { firestore, bucket } = options\n return {\n bucketName: String(bucket?.name ?? ''),\n readMedia: (collection, scopeId, mediaId) =>\n firestore\n .collection(collection)\n .doc(scopeId)\n .collection('media')\n .doc(mediaId)\n .get(),\n orgIdForHost: async (hostId) => {\n const index = await firestore.collection('hostIndex').doc(hostId).get()\n const orgId = index.exists ? index.get('orgId') : undefined\n return typeof orgId === 'string' && orgId ? orgId : null\n },\n signStorageRead: async (objectPath, expiresAtMs, signOptions) => {\n const [url] = await bucket.file(objectPath).getSignedUrl({\n version: 'v4',\n action: 'read',\n expires: expiresAtMs,\n ...(signOptions?.attachment ? { responseDisposition: 'attachment' } : {}),\n })\n return String(url)\n },\n }\n}\n"],"names":["hostQualifiedScope","MEDIA_CDN_RENDITION_AUTO","MEDIA_CDN_RENDITION_PARAM","MEDIA_CDN_ROUTE","paidMediaAssetOfObject","parseMediaRenditions","parsePaidMediaSource","mediaDeliveryProvider","lookalikeBrandForHost","mediaDeliveryRedirect","assertMediaSignatureTtl","mediaSignatureQuery","mintMediaSignature","isMediaStoragePathInScope","mediaCdnScopeRefusal","parseMediaCdnScope","selectAutoRendition","ACTIVE_OBJECT_EXTENSION","storageReadIsAttachment","objectPath","cdnParams","some","key","value","test","resolvePaidMediaDelivery","options","hostId","ttlMs","io","nowMs","Date","now","refuse","refusal","ok","orgId","siteOrgId","orgIdForHost","deliverFromProvider","delivery","asset","provider","undefined","requested","find","renditions","document","get","rendition","accept","entry","redirect","collection","scopeId","mediaId","servedType","contentType","claims","scope","deliveryScope","orgScope","enabled","deliveryEnabled","catch","via","location","expiresAtMs","deliverAsset","parsed","isOrg","owner","deliveryParsed","readMedia","exists","delivered","signature","params","URLSearchParams","set","prefix","toString","exp","source","stored","kind","externalHost","URL","url","hostname","host","bucketName","bucket","segments","split","base","signStorageRead","attachment","createPaidMediaDeliveryIo","firestore","String","name","doc","index","signOptions","file","getSignedUrl","version","action","expires","responseDisposition"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/paid-media-delivery.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 {\n hostQualifiedScope,\n MEDIA_CDN_RENDITION_AUTO,\n MEDIA_CDN_RENDITION_PARAM,\n MEDIA_CDN_ROUTE,\n paidMediaAssetOfObject,\n parseMediaRenditions,\n parsePaidMediaSource,\n} from '@aglyn/aglyn/server'\nimport {\n type MediaDeliveryProvider,\n mediaDeliveryProvider,\n} from '@aglyn/aglyn/plugin-manager/media-delivery-provider'\nimport { lookalikeBrandForHost } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { mediaDeliveryRedirect } from '@aglyn/tenant-data-admin/server/media-delivery'\nimport {\n assertMediaSignatureTtl,\n mediaSignatureQuery,\n mintMediaSignature,\n} from '@aglyn/tenant-data-admin/server/media-signing'\nimport { isMediaStoragePathInScope } from '@aglyn/tenant-data-admin/server/media-storage-path'\nimport {\n mediaCdnScopeRefusal,\n parseMediaCdnScope,\n selectAutoRendition,\n} from '@aglyn/tenant-data-admin/server/serve-media-cdn'\n\n/**\n * Turning a URL stored on a product into a link a buyer may follow (AGL-2814).\n *\n * A product's paid media — its members videos — is stored as whatever the\n * author's picker wrote: a media reference, a CDN path, a raw Storage download\n * URL, or a hotlink somebody typed. `parsePaidMediaSource` says which of those\n * a value is. The commerce routes verify who is entitled and then redirect;\n * this module decides WHERE they may redirect, and its whole job is that the\n * answer is never a URL that outlives the purchase.\n *\n * ## The rules, in the order they are applied\n *\n * 1. **A library asset is delivered only when it is PRIVATE**, as a CDN URL\n * signed with {@link mintMediaSignature}. The CDN refuses a private asset on\n * every path without an unexpired signature, which is what makes the link\n * die. A public asset is refused here instead of redirected to: its URL\n * works for anyone forever, so handing it to a buyer is the leak itself.\n * 2. **A Storage download URL is never redirected to.** Its token lives as\n * long as the object. When the object belongs to a library asset, rule 1\n * applies to that asset. When no asset owns it (a legacy object, a derived\n * rendition file), it gets a V4 signed read URL with the same bounded\n * lifetime. When it is not in this platform's bucket, or not inside the\n * site's own library or its org's, it is refused.\n * 3. **Only the requesting site's own libraries are signed.** A product is\n * author data. Signing whatever scope it names would let one site's author\n * mint working links to another org's private files, so a host scope must\n * be this site and an org scope must be this site's org, and the signed\n * scope is always qualified with this site so the CDN's `visibleTo` check\n * runs against the site actually selling it.\n * 4. **A hotlink to somebody else's server passes through unchanged.** No code\n * of ours runs there and nothing here can make its URL expire. That is the\n * author's hosting decision, and the docs say so. EXCEPT a hotlink whose\n * host wears a brand it is not (`lookalikeBrandForHost`, the phishing\n * screen's rule): the redirect leaves from the merchant's own domain, so a\n * lure behind it is one no screen of the page or the receipt ever saw. It\n * is refused for every workspace, like a lookalike link in an email\n * (AGL-3363), and the caller files it.\n * 5. **A signed Storage read is a download when it is one.** A download, or\n * an object whose type a browser would run (HTML, SVG, XML), is signed\n * with `response-content-disposition: attachment`, so the file is saved\n * rather than rendered (AGL-3363). The CDN already does this for its own\n * deliveries; a V4 URL carried nothing.\n *\n * ## A video with a copy at the delivery provider (AGL-2824)\n *\n * A caller that asks for it with `delivery` gets, for a video that passes\n * rule 1, the delivery provider's own short-lived URL instead of the signed CDN\n * URL, whenever `mediaDeliveryRedirect` finds a current copy and the release\n * flag is on for the owning org. It is the URL the CDN would redirect the\n * signed request to anyway, minted one hop earlier and bounded by the same\n * lifetime. Anything else takes the signed CDN URL exactly as before.\n */\n\n/** Why a stored URL was not turned into a link. */\nexport type PaidMediaRefusal =\n /** A library asset the public can still fetch without a signature. */\n | 'not-private'\n /** No such asset, a deleted one, or one not shared with this site. */\n | 'not-found'\n /** Another site's library, another org's, or outside the media prefixes. */\n | 'out-of-scope'\n /** A Storage URL for a bucket this platform does not serve from. */\n | 'foreign-storage'\n /** Not a value any delivery can be built from. */\n | 'malformed'\n /** A hotlink to a host that wears another brand's name (AGL-3363). */\n | 'lookalike'\n\nexport type PaidMediaDelivery =\n | {\n ok: true\n via: 'signed-cdn'\n location: string\n expiresAtMs: number\n /** The scope segment the signature covers. */\n scope: string\n mediaId: string\n }\n | {\n ok: true\n via: 'signed-storage'\n location: string\n expiresAtMs: number\n objectPath: string\n /** Whose library the object is in, for the bandwidth count. */\n collection: 'hosts' | 'orgs'\n scopeId: string\n /** The object's size, 0 when Storage did not say. */\n sizeBytes: number\n }\n | { ok: true; via: 'external'; location: string }\n | {\n ok: true\n via: 'delivery'\n location: string\n expiresAtMs: number\n /** The scope segment the delivery was minted for. */\n scope: string\n mediaId: string\n /** Whose library the asset is in, for the bandwidth count. */\n collection: 'hosts' | 'orgs'\n scopeId: string\n /** The size of the copy the provider serves. */\n sizeBytes: number\n }\n | {\n ok: false\n refusal: PaidMediaRefusal\n /** For a `lookalike`: the host it wears a brand in, for the caller to file. */\n host?: string\n }\n\n/** A document read, reduced to what this module asks of one. */\nexport interface PaidMediaDocument {\n exists: boolean\n get(field: string): unknown\n}\n\n/** Every read and signature this module needs, injectable for tests. */\nexport interface PaidMediaDeliveryIo {\n /** The platform's media bucket; empty when unconfigured. */\n bucketName: string\n readMedia(\n collection: 'hosts' | 'orgs',\n scopeId: string,\n mediaId: string,\n ): Promise<PaidMediaDocument>\n /** The org a site belongs to, or null for an unindexed site. */\n orgIdForHost(hostId: string): Promise<string | null>\n /**\n * A V4 signed read URL for one object in {@link bucketName}. `attachment`\n * signs `response-content-disposition: attachment` into it (AGL-3363).\n */\n signStorageRead(\n objectPath: string,\n expiresAtMs: number,\n options?: { attachment?: boolean },\n ): Promise<string>\n /**\n * The object's size in bytes, for the bandwidth a signed read can send;\n * 0 when it cannot be read. Optional, so a double that never signs a\n * Storage read need not answer it.\n */\n storageObjectSize?(objectPath: string): Promise<number>\n}\n\n/** Object types a browser runs rather than shows: always saved, never rendered. */\nconst ACTIVE_OBJECT_EXTENSION = /\\.(html?|xhtml|xht|svgz?|xml|xsl|xslt|mht|mhtml|js|mjs)$/i\n\n/** Is this signed read a download, by the caller's ask or by what the object is? */\nexport function storageReadIsAttachment(\n objectPath: string,\n cdnParams: ReadonlyArray<readonly [string, string]> | undefined,\n): boolean {\n if ((cdnParams ?? []).some(([key, value]) => key === 'download' && value === '1')) {\n return true\n }\n return ACTIVE_OBJECT_EXTENSION.test(objectPath)\n}\n\n/**\n * Resolves a stored product media URL to a redirect target that expires, or\n * says why there is none. See the module comment for the rules.\n *\n * `cdnParams` ride on a signed CDN URL ahead of the signature, which covers\n * `(scope, mediaId, exp)` and never the parameters, so they select a\n * representation of the one asset the signature names and nothing else.\n */\nexport async function resolvePaidMediaDelivery(options: {\n stored: unknown\n /** The site selling the media; the only libraries it may sign from. */\n hostId: string\n /** The lifetime of the link; bounded by `assertMediaSignatureTtl`. */\n ttlMs: number\n cdnParams?: ReadonlyArray<readonly [string, string]>\n /**\n * Hand a video with a current copy at the delivery provider its URL there\n * (AGL-2824). `accept` is the request's `Accept`, which chooses the\n * rendition when `cdnParams` asks for `?r=auto`. The provider and the flag\n * verdict default to the registered provider and the release flag.\n */\n delivery?: {\n accept?: unknown\n provider?: MediaDeliveryProvider | null\n enabled?: (orgId: string | null) => Promise<boolean>\n }\n nowMs?: number\n io: PaidMediaDeliveryIo\n}): Promise<PaidMediaDelivery> {\n const { hostId, ttlMs, io } = options\n const nowMs = options.nowMs ?? Date.now()\n const refuse = (refusal: PaidMediaRefusal): PaidMediaDelivery => ({\n ok: false,\n refusal,\n })\n if (!hostId) return refuse('out-of-scope')\n\n // One lookup per call, and only for the forms that need to know the org.\n let orgId: Promise<string | null> | undefined\n const siteOrgId = () => (orgId ??= io.orgIdForHost(hostId))\n\n /**\n * The delivery provider's URL for a private library video, or null for\n * the signed CDN URL. The representation is the one the CDN would pick for\n * the same `?r=` and `Accept`, so the buyer gets the copy the signed URL\n * would have been redirected to.\n */\n const deliverFromProvider = async (\n delivery: NonNullable<typeof options.delivery>,\n asset: {\n collection: 'hosts' | 'orgs'\n scopeId: string\n orgScope: boolean\n deliveryScope: string\n mediaId: string\n document: PaidMediaDocument\n },\n ): Promise<PaidMediaDelivery | null> => {\n const provider =\n delivery.provider === undefined ? mediaDeliveryProvider('deliver') : delivery.provider\n if (!provider) return null\n const requested = (options.cdnParams ?? []).find(\n ([key]) => key === MEDIA_CDN_RENDITION_PARAM,\n )?.[1]\n const renditions = parseMediaRenditions(asset.document.get('videoRenditions'))\n const rendition = !requested\n ? undefined\n : requested === MEDIA_CDN_RENDITION_AUTO\n ? selectAutoRendition(renditions, delivery.accept)\n : renditions.find((entry) => entry.key === requested)\n const redirect = await mediaDeliveryRedirect({\n provider,\n asset: {\n collection: asset.collection,\n scopeId: asset.scopeId,\n mediaId: asset.mediaId,\n },\n document: asset.document,\n rendition,\n servedType: rendition ? rendition.contentType : asset.document.get('contentType'),\n claims: { scope: asset.deliveryScope, mediaId: asset.mediaId, hostId },\n orgId: async () => (asset.orgScope ? asset.scopeId : await siteOrgId()),\n ...(delivery.enabled ? { deliveryEnabled: delivery.enabled } : {}),\n ttlMs,\n nowMs,\n }).catch(() => null)\n if (!redirect) return null\n return {\n ok: true,\n via: 'delivery',\n location: redirect.location,\n expiresAtMs: redirect.expiresAtMs,\n scope: asset.deliveryScope,\n mediaId: asset.mediaId,\n collection: asset.collection,\n scopeId: asset.scopeId,\n sizeBytes: redirect.sizeBytes,\n }\n }\n\n const deliverAsset = async (\n scope: string,\n mediaId: string,\n ): Promise<PaidMediaDelivery> => {\n const parsed = parseMediaCdnScope(scope)\n if (!parsed) return refuse('malformed')\n let collection: 'hosts' | 'orgs'\n let deliveryScope: string\n if (parsed.isOrg) {\n const owner = await siteOrgId()\n if (!owner || parsed.scopeId !== owner) return refuse('out-of-scope')\n collection = 'orgs'\n deliveryScope = hostQualifiedScope(scope, hostId)\n } else {\n if (parsed.scopeId !== hostId) return refuse('out-of-scope')\n collection = 'hosts'\n deliveryScope = hostId\n }\n const deliveryParsed = parseMediaCdnScope(deliveryScope)\n if (!deliveryParsed) return refuse('malformed')\n const document = await io.readMedia(collection, parsed.scopeId, mediaId)\n if (!document?.exists || document.get('deletedAt')) {\n return refuse('not-found')\n }\n // The CDN would 404 an org asset this site is not allowed to use, so a\n // link to one is a link that cannot play.\n if (mediaCdnScopeRefusal(deliveryParsed, document.get('visibleTo'))) {\n return refuse('not-found')\n }\n if (document.get('private') !== true) return refuse('not-private')\n assertMediaSignatureTtl(ttlMs)\n const delivered = options.delivery\n ? await deliverFromProvider(options.delivery, {\n collection,\n scopeId: parsed.scopeId,\n orgScope: parsed.isOrg,\n deliveryScope,\n mediaId,\n document,\n })\n : null\n if (delivered) return delivered\n const signature = mintMediaSignature(deliveryScope, mediaId, nowMs, ttlMs)\n const params = new URLSearchParams()\n for (const [key, value] of options.cdnParams ?? []) {\n if (key !== 'exp' && key !== 'sig') params.set(key, value)\n }\n const prefix = params.toString()\n return {\n ok: true,\n via: 'signed-cdn',\n location:\n `${MEDIA_CDN_ROUTE}/${deliveryScope}/${mediaId}?` +\n `${prefix ? `${prefix}&` : ''}${mediaSignatureQuery(signature)}`,\n expiresAtMs: signature.exp,\n scope: deliveryScope,\n mediaId,\n }\n }\n\n const source = parsePaidMediaSource(options.stored)\n switch (source.kind) {\n case 'malformed':\n return refuse('malformed')\n case 'external': {\n let externalHost: string\n try {\n externalHost = new URL(source.url).hostname\n } catch {\n return refuse('malformed')\n }\n if (lookalikeBrandForHost(externalHost)) {\n return { ok: false, refusal: 'lookalike', host: externalHost }\n }\n return { ok: true, via: 'external', location: source.url }\n }\n case 'asset':\n return deliverAsset(source.scope, source.mediaId)\n case 'storage-object': {\n if (!io.bucketName || source.bucket !== io.bucketName) {\n return refuse('foreign-storage')\n }\n const { objectPath } = source\n const segments = objectPath.split('/')\n let base: string | null = null\n if (segments[0] === 'hosts' && segments[1] === hostId) {\n base = `hosts/${hostId}`\n } else if (segments[0] === 'orgs' && segments[1]) {\n const owner = await siteOrgId()\n if (owner && segments[1] === owner) base = `orgs/${owner}`\n }\n // The same prefix test the CDN applies to a document's `storagePath`,\n // which also refuses traversal and empty segments.\n if (!base || !isMediaStoragePathInScope(objectPath, base)) {\n return refuse('out-of-scope')\n }\n // An object keyed like an asset IS that asset, and the asset's own rules\n // decide — including refusing it when the asset is still public.\n const asset = paidMediaAssetOfObject(objectPath)\n if (asset) return deliverAsset(asset.scope, asset.mediaId)\n assertMediaSignatureTtl(ttlMs)\n const expiresAtMs = nowMs + ttlMs\n const [location, sizeBytes] = await Promise.all([\n io.signStorageRead(objectPath, expiresAtMs, {\n attachment: storageReadIsAttachment(objectPath, options.cdnParams),\n }),\n io.storageObjectSize?.(objectPath).catch(() => 0) ?? Promise.resolve(0),\n ])\n return {\n ok: true,\n via: 'signed-storage',\n location,\n expiresAtMs,\n objectPath,\n collection: segments[0] === 'orgs' ? 'orgs' : 'hosts',\n scopeId: segments[1],\n sizeBytes,\n }\n }\n }\n}\n\n/**\n * The production reads behind {@link resolvePaidMediaDelivery}, built on the\n * caller's Firestore and bucket handles so a route's own mocks reach them.\n */\nexport function createPaidMediaDeliveryIo(options: {\n firestore: {\n collection(name: string): {\n doc(id: string): {\n get(): Promise<PaidMediaDocument>\n collection(name: string): {\n doc(id: string): { get(): Promise<PaidMediaDocument> }\n }\n }\n }\n }\n bucket: {\n name?: string\n file(path: string): {\n getSignedUrl(config: {\n version: 'v4'\n action: 'read'\n expires: number\n responseDisposition?: string\n }): Promise<[string] | string[]>\n getMetadata?(): Promise<[{ size?: unknown }, ...unknown[]] | Array<{ size?: unknown }>>\n }\n }\n}): PaidMediaDeliveryIo {\n const { firestore, bucket } = options\n return {\n bucketName: String(bucket?.name ?? ''),\n readMedia: (collection, scopeId, mediaId) =>\n firestore\n .collection(collection)\n .doc(scopeId)\n .collection('media')\n .doc(mediaId)\n .get(),\n orgIdForHost: async (hostId) => {\n const index = await firestore.collection('hostIndex').doc(hostId).get()\n const orgId = index.exists ? index.get('orgId') : undefined\n return typeof orgId === 'string' && orgId ? orgId : null\n },\n signStorageRead: async (objectPath, expiresAtMs, signOptions) => {\n const [url] = await bucket.file(objectPath).getSignedUrl({\n version: 'v4',\n action: 'read',\n expires: expiresAtMs,\n ...(signOptions?.attachment ? { responseDisposition: 'attachment' } : {}),\n })\n return String(url)\n },\n storageObjectSize: async (objectPath) => {\n const file = bucket.file(objectPath)\n if (!file.getMetadata) return 0\n const [metadata] = await file.getMetadata()\n const size = Number(metadata?.size)\n return Number.isSafeInteger(size) && size > 0 ? size : 0\n },\n }\n}\n"],"names":["hostQualifiedScope","MEDIA_CDN_RENDITION_AUTO","MEDIA_CDN_RENDITION_PARAM","MEDIA_CDN_ROUTE","paidMediaAssetOfObject","parseMediaRenditions","parsePaidMediaSource","mediaDeliveryProvider","lookalikeBrandForHost","mediaDeliveryRedirect","assertMediaSignatureTtl","mediaSignatureQuery","mintMediaSignature","isMediaStoragePathInScope","mediaCdnScopeRefusal","parseMediaCdnScope","selectAutoRendition","ACTIVE_OBJECT_EXTENSION","storageReadIsAttachment","objectPath","cdnParams","some","key","value","test","resolvePaidMediaDelivery","options","hostId","ttlMs","io","nowMs","Date","now","refuse","refusal","ok","orgId","siteOrgId","orgIdForHost","deliverFromProvider","delivery","asset","provider","undefined","requested","find","renditions","document","get","rendition","accept","entry","redirect","collection","scopeId","mediaId","servedType","contentType","claims","scope","deliveryScope","orgScope","enabled","deliveryEnabled","catch","via","location","expiresAtMs","sizeBytes","deliverAsset","parsed","isOrg","owner","deliveryParsed","readMedia","exists","delivered","signature","params","URLSearchParams","set","prefix","toString","exp","source","stored","kind","externalHost","URL","url","hostname","host","bucketName","bucket","segments","split","base","Promise","all","signStorageRead","attachment","storageObjectSize","resolve","createPaidMediaDeliveryIo","firestore","String","name","doc","index","signOptions","file","getSignedUrl","version","action","expires","responseDisposition","getMetadata","metadata","size","Number","isSafeInteger"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { recordMediaServe } from '@aglyn/tenant-data-admin/server/media-serve-count';
|
|
18
|
+
import type { PaidMediaDelivery } from './paid-media-delivery';
|
|
19
|
+
/**
|
|
20
|
+
* Counts the bandwidth a paid link sends from somewhere other than our CDN
|
|
21
|
+
* route (AGL-3474). A delivery provider's URL and a signed Storage URL are
|
|
22
|
+
* fetched straight from the provider or the bucket, so the request that hands
|
|
23
|
+
* one out is the only one we see, and it counts what the link opens at full
|
|
24
|
+
* size. A signed CDN URL is not counted here: its bytes reach `serveMediaCdn`,
|
|
25
|
+
* which counts them. Awaited after the redirect, so the function lives until
|
|
26
|
+
* the count is written.
|
|
27
|
+
*/
|
|
28
|
+
export declare function countOffRouteServe(firestore: Parameters<typeof recordMediaServe>[0]['firestore'], delivery: PaidMediaDelivery): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
*/ import { recordMediaServe } from "@aglyn/tenant-data-admin/server/media-serve-count";
|
|
17
|
+
/**
|
|
18
|
+
* Counts the bandwidth a paid link sends from somewhere other than our CDN
|
|
19
|
+
* route (AGL-3474). A delivery provider's URL and a signed Storage URL are
|
|
20
|
+
* fetched straight from the provider or the bucket, so the request that hands
|
|
21
|
+
* one out is the only one we see, and it counts what the link opens at full
|
|
22
|
+
* size. A signed CDN URL is not counted here: its bytes reach `serveMediaCdn`,
|
|
23
|
+
* which counts them. Awaited after the redirect, so the function lives until
|
|
24
|
+
* the count is written.
|
|
25
|
+
*/ export async function countOffRouteServe(firestore, delivery) {
|
|
26
|
+
if (delivery.ok !== true) return;
|
|
27
|
+
if (delivery.via === 'delivery') {
|
|
28
|
+
await recordMediaServe({
|
|
29
|
+
firestore,
|
|
30
|
+
collection: delivery.collection,
|
|
31
|
+
scopeId: delivery.scopeId,
|
|
32
|
+
mediaId: delivery.mediaId,
|
|
33
|
+
bandwidthBytes: delivery.sizeBytes,
|
|
34
|
+
redirect: true
|
|
35
|
+
});
|
|
36
|
+
} else if (delivery.via === 'signed-storage' && delivery.sizeBytes > 0) {
|
|
37
|
+
await recordMediaServe({
|
|
38
|
+
firestore,
|
|
39
|
+
collection: delivery.collection,
|
|
40
|
+
scopeId: delivery.scopeId,
|
|
41
|
+
bandwidthBytes: delivery.sizeBytes
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=paid-media-serve-count.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/paid-media-serve-count.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 { recordMediaServe } from '@aglyn/tenant-data-admin/server/media-serve-count'\nimport type { PaidMediaDelivery } from './paid-media-delivery'\n\n/**\n * Counts the bandwidth a paid link sends from somewhere other than our CDN\n * route (AGL-3474). A delivery provider's URL and a signed Storage URL are\n * fetched straight from the provider or the bucket, so the request that hands\n * one out is the only one we see, and it counts what the link opens at full\n * size. A signed CDN URL is not counted here: its bytes reach `serveMediaCdn`,\n * which counts them. Awaited after the redirect, so the function lives until\n * the count is written.\n */\nexport async function countOffRouteServe(\n firestore: Parameters<typeof recordMediaServe>[0]['firestore'],\n delivery: PaidMediaDelivery,\n): Promise<void> {\n if (delivery.ok !== true) return\n if (delivery.via === 'delivery') {\n await recordMediaServe({\n firestore,\n collection: delivery.collection,\n scopeId: delivery.scopeId,\n mediaId: delivery.mediaId,\n bandwidthBytes: delivery.sizeBytes,\n redirect: true,\n })\n } else if (delivery.via === 'signed-storage' && delivery.sizeBytes > 0) {\n await recordMediaServe({\n firestore,\n collection: delivery.collection,\n scopeId: delivery.scopeId,\n bandwidthBytes: delivery.sizeBytes,\n })\n }\n}\n"],"names":["recordMediaServe","countOffRouteServe","firestore","delivery","ok","via","collection","scopeId","mediaId","bandwidthBytes","sizeBytes","redirect"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,oDAAmD;AAGpF;;;;;;;;CAQC,GACD,OAAO,eAAeC,mBACpBC,SAA8D,EAC9DC,QAA2B;IAE3B,IAAIA,SAASC,EAAE,KAAK,MAAM;IAC1B,IAAID,SAASE,GAAG,KAAK,YAAY;QAC/B,MAAML,iBAAiB;YACrBE;YACAI,YAAYH,SAASG,UAAU;YAC/BC,SAASJ,SAASI,OAAO;YACzBC,SAASL,SAASK,OAAO;YACzBC,gBAAgBN,SAASO,SAAS;YAClCC,UAAU;QACZ;IACF,OAAO,IAAIR,SAASE,GAAG,KAAK,oBAAoBF,SAASO,SAAS,GAAG,GAAG;QACtE,MAAMV,iBAAiB;YACrBE;YACAI,YAAYH,SAASG,UAAU;YAC/BC,SAASJ,SAASI,OAAO;YACzBE,gBAAgBN,SAASO,SAAS;QACpC;IACF;AACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
|
|
18
|
+
/**
|
|
19
|
+
* COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* An order is the merchant's record of a sale: the amounts, the line items
|
|
22
|
+
* and the tax are theirs to keep and the law expects them kept. So the person
|
|
23
|
+
* is taken OFF every order they placed on a site of the workspace — name,
|
|
24
|
+
* email, both addresses — and a stamp says when; the order itself stays.
|
|
25
|
+
*
|
|
26
|
+
* The address also lives in what the orders list queries by (AGL-3321): its
|
|
27
|
+
* lower-cased key, its search prefixes, and the quick search's token array,
|
|
28
|
+
* which holds those prefixes beside the order number's and the item names'.
|
|
29
|
+
* The first two go; the prefixes are taken back out of the third. A prefix
|
|
30
|
+
* the number or an item name shares with the address goes with it — a search
|
|
31
|
+
* for that prefix misses this order until the orders backfill restamps it —
|
|
32
|
+
* and nothing left on the order answers to the address.
|
|
33
|
+
*
|
|
34
|
+
* A DRY RUN counts the orders and writes nothing.
|
|
35
|
+
*/
|
|
36
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
37
|
+
export interface CommercePersonEraserDeps {
|
|
38
|
+
firestore(): Firestore;
|
|
39
|
+
}
|
|
40
|
+
/** The patch that takes the buyer off one order, as it stands. */
|
|
41
|
+
export declare function erasedOrderPatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
|
|
42
|
+
export declare function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser;
|
|
43
|
+
/** The eraser over the platform's own Firestore, as the declarations register it. */
|
|
44
|
+
export declare const commercePersonEraser: PluginPersonEraser;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { updateWhereEquals } from "@aglyn/tenant-data-admin/server/paged-sweeps";
|
|
18
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
19
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
20
|
+
const LABEL = 'commerce person eraser';
|
|
21
|
+
/** The patch that takes the buyer off one order, as it stands. */ export function erasedOrderPatch(data, now) {
|
|
22
|
+
const addressTokens = new Set(Array.isArray(data == null ? void 0 : data['customerEmailTokens']) ? data['customerEmailTokens'] : []);
|
|
23
|
+
return _extends({
|
|
24
|
+
customerEmail: null,
|
|
25
|
+
customerName: null,
|
|
26
|
+
shippingAddress: FieldValue.delete(),
|
|
27
|
+
billingAddress: FieldValue.delete(),
|
|
28
|
+
customerErasedAtMs: now,
|
|
29
|
+
customerEmailLower: null,
|
|
30
|
+
customerEmailTokens: []
|
|
31
|
+
}, Array.isArray(data == null ? void 0 : data['searchTokens']) ? {
|
|
32
|
+
searchTokens: data['searchTokens'].filter((token)=>!addressTokens.has(token))
|
|
33
|
+
} : {});
|
|
34
|
+
}
|
|
35
|
+
export function createCommercePersonEraser(deps) {
|
|
36
|
+
return async ({ orgId, email, dryRun, atMs })=>{
|
|
37
|
+
const db = deps.firestore();
|
|
38
|
+
const hosts = await db.collection('hosts').where('orgId', '==', orgId).get();
|
|
39
|
+
let orders = 0;
|
|
40
|
+
for (const host of hosts.docs){
|
|
41
|
+
const placed = host.ref.collection('orders');
|
|
42
|
+
if (dryRun) {
|
|
43
|
+
orders += (await placed.where('customerEmail', '==', email).get()).size;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
orders += await updateWhereEquals(db, placed, 'customerEmail', email, (data)=>erasedOrderPatch(data, atMs), LABEL);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
orders
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The eraser over the platform's own Firestore, as the declarations register it. */ export const commercePersonEraser = (request)=>createCommercePersonEraser({
|
|
54
|
+
firestore: ()=>firebaseAdmin.app().firestore()
|
|
55
|
+
})(request);
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=person-eraser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/person-eraser.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 type {\n PluginPersonEraser,\n PluginPersonErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { updateWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).\n *\n * An order is the merchant's record of a sale: the amounts, the line items\n * and the tax are theirs to keep and the law expects them kept. So the person\n * is taken OFF every order they placed on a site of the workspace — name,\n * email, both addresses — and a stamp says when; the order itself stays.\n *\n * The address also lives in what the orders list queries by (AGL-3321): its\n * lower-cased key, its search prefixes, and the quick search's token array,\n * which holds those prefixes beside the order number's and the item names'.\n * The first two go; the prefixes are taken back out of the third. A prefix\n * the number or an item name shares with the address goes with it — a search\n * for that prefix misses this order until the orders backfill restamps it —\n * and nothing left on the order answers to the address.\n *\n * A DRY RUN counts the orders and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CommercePersonEraserDeps {\n firestore(): Firestore\n}\n\nconst LABEL = 'commerce person eraser'\n\n/** The patch that takes the buyer off one order, as it stands. */\nexport function erasedOrderPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const addressTokens = new Set(\n Array.isArray(data?.['customerEmailTokens']) ? data['customerEmailTokens'] : [],\n )\n return {\n customerEmail: null,\n customerName: null,\n shippingAddress: FieldValue.delete(),\n billingAddress: FieldValue.delete(),\n customerErasedAtMs: now,\n customerEmailLower: null,\n customerEmailTokens: [],\n ...(Array.isArray(data?.['searchTokens'])\n ? {\n searchTokens: (data['searchTokens'] as unknown[]).filter(\n (token) => !addressTokens.has(token),\n ),\n }\n : {}),\n }\n}\n\nexport function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser {\n return async ({ orgId, email, dryRun, atMs }): Promise<PluginPersonErasureReport> => {\n const db = deps.firestore()\n const hosts = await db.collection('hosts').where('orgId', '==', orgId).get()\n let orders = 0\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n continue\n }\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n }\n return { orders }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const commercePersonEraser: PluginPersonEraser = (request) =>\n createCommercePersonEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["updateWhereEquals","FieldValue","firebaseAdmin","LABEL","erasedOrderPatch","data","now","addressTokens","Set","Array","isArray","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","host","docs","placed","ref","size","commercePersonEraser","request","app"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD,SAASA,iBAAiB,QAAQ,+CAA8C;AAChF,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,aAAa,QAAQ,iDAAgD;AA2B9E,MAAMC,QAAQ;AAEd,gEAAgE,GAChE,OAAO,SAASC,iBACdC,IAAgD,EAChDC,GAAW;IAEX,MAAMC,gBAAgB,IAAIC,IACxBC,MAAMC,OAAO,CAACL,wBAAAA,IAAM,CAAC,sBAAsB,IAAIA,IAAI,CAAC,sBAAsB,GAAG,EAAE;IAEjF,OAAO;QACLM,eAAe;QACfC,cAAc;QACdC,iBAAiBZ,WAAWa,MAAM;QAClCC,gBAAgBd,WAAWa,MAAM;QACjCE,oBAAoBV;QACpBW,oBAAoB;QACpBC,qBAAqB,EAAE;OACnBT,MAAMC,OAAO,CAACL,wBAAAA,IAAM,CAAC,eAAe,IACpC;QACEc,cAAc,AAACd,IAAI,CAAC,eAAe,CAAee,MAAM,CACtD,CAACC,QAAU,CAACd,cAAce,GAAG,CAACD;IAElC,IACA,CAAC;AAET;AAEA,OAAO,SAASE,2BAA2BC,IAA8B;IACvE,OAAO,OAAO,EAAEC,KAAK,EAAEC,KAAK,EAAEC,MAAM,EAAEC,IAAI,EAAE;QAC1C,MAAMC,KAAKL,KAAKM,SAAS;QACzB,MAAMC,QAAQ,MAAMF,GAAGG,UAAU,CAAC,SAASC,KAAK,CAAC,SAAS,MAAMR,OAAOS,GAAG;QAC1E,IAAIC,SAAS;QACb,KAAK,MAAMC,QAAQL,MAAMM,IAAI,CAAE;YAC7B,MAAMC,SAASF,KAAKG,GAAG,CAACP,UAAU,CAAC;YACnC,IAAIL,QAAQ;gBACVQ,UAAU,AAAC,CAAA,MAAMG,OAAOL,KAAK,CAAC,iBAAiB,MAAMP,OAAOQ,GAAG,EAAC,EAAGM,IAAI;gBACvE;YACF;YACAL,UAAU,MAAMnC,kBACd6B,IACAS,QACA,iBACAZ,OACA,CAACrB,OAASD,iBAAiBC,MAAMuB,OACjCzB;QAEJ;QACA,OAAO;YAAEgC;QAAO;IAClB;AACF;AAEA,mFAAmF,GACnF,OAAO,MAAMM,uBAA2C,CAACC,UACvDnB,2BAA2B;QAAEO,WAAW,IAAM5B,cAAcyC,GAAG,GAAGb,SAAS;IAAG,GAAGY,SAAQ"}
|
package/src/lib/server/refund.js
CHANGED
|
@@ -19,14 +19,10 @@ import * as CommerceModel from "../model/index.js";
|
|
|
19
19
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
20
20
|
import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
|
|
21
21
|
import { createHash } from "crypto";
|
|
22
|
-
import {
|
|
22
|
+
import { recordPluginPersonRefund } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
23
23
|
import { applyGiftCardRiskToOrder } from "./gift-card-risk.js";
|
|
24
24
|
import { flagOrderRestock } from "./restock-flag.js";
|
|
25
|
-
|
|
26
|
-
// `updateExisting`: the specs in this library mock `@aglyn/tenant-data-admin`
|
|
27
|
-
// wholesale, and a permissive stub would turn a reversal that never happened
|
|
28
|
-
// green.
|
|
29
|
-
import { reverseEmailAttributedRevenue } from "@aglyn/tenant-data-admin/server/email-revenue-attribution";
|
|
25
|
+
import { reverseOrderConversion } from "@aglyn/aglyn/plugin-manager/plugin-conversion-credit";
|
|
30
26
|
/**
|
|
31
27
|
* Retires the licence keys a refund withdrew (AGL-2454).
|
|
32
28
|
*
|
|
@@ -561,9 +557,13 @@ import { reverseEmailAttributedRevenue } from "@aglyn/tenant-data-admin/server/e
|
|
|
561
557
|
// the recorded 200 instead of being turned away with "already being
|
|
562
558
|
// processed". Awaited rather than fired off with `void` — the handler is
|
|
563
559
|
// serverless, and work left running past the response is work the
|
|
564
|
-
// container may be frozen before it finishes.
|
|
565
|
-
//
|
|
566
|
-
//
|
|
560
|
+
// container may be frozen before it finishes. The seam never throws, so
|
|
561
|
+
// awaiting adds no way for this to fail a refund that has already left
|
|
562
|
+
// the merchant's account.
|
|
563
|
+
//
|
|
564
|
+
// Through the plugin that keeps people (AGL-3080): what a refund does to
|
|
565
|
+
// the customer's record is that plugin's to decide, and commerce writes
|
|
566
|
+
// none of it.
|
|
567
567
|
//
|
|
568
568
|
// Amount is THIS attempt's `refundCents`, already capped against what was
|
|
569
569
|
// left, so several partials sum to at most the order total — the same
|
|
@@ -571,12 +571,12 @@ import { reverseEmailAttributedRevenue } from "@aglyn/tenant-data-admin/server/e
|
|
|
571
571
|
// cases need no key of their own: a keyed retry never reaches here (it
|
|
572
572
|
// replays at the claim), and a keyless one is a genuinely new refund that
|
|
573
573
|
// moved more money and should be counted.
|
|
574
|
-
await
|
|
574
|
+
await recordPluginPersonRefund({
|
|
575
575
|
hostId,
|
|
576
|
-
orderId,
|
|
576
|
+
refId: orderId,
|
|
577
577
|
email: order.customerEmail,
|
|
578
578
|
amountCents: refundCents,
|
|
579
|
-
closedTheOrder
|
|
579
|
+
closedTheSale: closedTheOrder
|
|
580
580
|
});
|
|
581
581
|
/*
|
|
582
582
|
* The campaign's side of the same ledger.
|
|
@@ -585,8 +585,8 @@ import { reverseEmailAttributedRevenue } from "@aglyn/tenant-data-admin/server/e
|
|
|
585
585
|
* or wholly wrong — a campaign cannot go on being paid for a sale the
|
|
586
586
|
* merchant reversed, and revenue attribution that only ever rises is the
|
|
587
587
|
* flattering half of a measurement. Recorded beside the gross rather than
|
|
588
|
-
* subtracted from it, for the reason
|
|
589
|
-
*
|
|
588
|
+
* subtracted from it, for the reason the customer's record keeps its
|
|
589
|
+
* refunds beside what they spent.
|
|
590
590
|
*
|
|
591
591
|
* Keyed by the ORDER and not by the buyer, so it needs no email and works
|
|
592
592
|
* for a guest checkout: the attribution record holds which campaign and
|
|
@@ -594,7 +594,7 @@ import { reverseEmailAttributedRevenue } from "@aglyn/tenant-data-admin/server/e
|
|
|
594
594
|
* swallow-all contract and same awaited call as the two ledgers around
|
|
595
595
|
* it — nothing here may fail a refund that has already left the
|
|
596
596
|
* merchant's account.
|
|
597
|
-
*/ await
|
|
597
|
+
*/ await reverseOrderConversion({
|
|
598
598
|
hostId,
|
|
599
599
|
orderId,
|
|
600
600
|
amountCents: refundCents,
|