@endora-commerce/mod-pwa 0.100.0
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/admin/api/pwa-client.d.ts +15 -0
- package/dist/admin/api/pwa-client.d.ts.map +1 -0
- package/dist/admin/api/pwa-client.js +56 -0
- package/dist/admin/api/pwa-client.js.map +1 -0
- package/dist/admin/components/PushAudienceRuleBuilder.d.ts +19 -0
- package/dist/admin/components/PushAudienceRuleBuilder.d.ts.map +1 -0
- package/dist/admin/components/PushAudienceRuleBuilder.js +211 -0
- package/dist/admin/components/PushAudienceRuleBuilder.js.map +1 -0
- package/dist/admin/index.d.ts +39 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +61 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/PwaPage.d.ts +16 -0
- package/dist/admin/pages/PwaPage.d.ts.map +1 -0
- package/dist/admin/pages/PwaPage.js +207 -0
- package/dist/admin/pages/PwaPage.js.map +1 -0
- package/dist/backend/entities/push-message-delivery.entity.d.ts +18 -0
- package/dist/backend/entities/push-message-delivery.entity.d.ts.map +1 -0
- package/dist/backend/entities/push-message-delivery.entity.js +68 -0
- package/dist/backend/entities/push-message-delivery.entity.js.map +1 -0
- package/dist/backend/entities/push-message.entity.d.ts +26 -0
- package/dist/backend/entities/push-message.entity.d.ts.map +1 -0
- package/dist/backend/entities/push-message.entity.js +102 -0
- package/dist/backend/entities/push-message.entity.js.map +1 -0
- package/dist/backend/entities/push-subscription.entity.d.ts +49 -0
- package/dist/backend/entities/push-subscription.entity.d.ts.map +1 -0
- package/dist/backend/entities/push-subscription.entity.js +117 -0
- package/dist/backend/entities/push-subscription.entity.js.map +1 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.d.ts +21 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.d.ts.map +1 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.js +69 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.js.map +1 -0
- package/dist/backend/index.d.ts +100 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +107 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +119 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +109 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/request-actor.d.ts +24 -0
- package/dist/backend/request-actor.d.ts.map +1 -0
- package/dist/backend/request-actor.js +29 -0
- package/dist/backend/request-actor.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +52 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +222 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +29 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +66 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/cross-module-context.d.ts +90 -0
- package/dist/backend/services/cross-module-context.d.ts.map +1 -0
- package/dist/backend/services/cross-module-context.js +84 -0
- package/dist/backend/services/cross-module-context.js.map +1 -0
- package/dist/backend/services/providers/web-push-provider.d.ts +31 -0
- package/dist/backend/services/providers/web-push-provider.d.ts.map +1 -0
- package/dist/backend/services/providers/web-push-provider.js +72 -0
- package/dist/backend/services/providers/web-push-provider.js.map +1 -0
- package/dist/backend/services/push-audience-evaluator.d.ts +24 -0
- package/dist/backend/services/push-audience-evaluator.d.ts.map +1 -0
- package/dist/backend/services/push-audience-evaluator.js +29 -0
- package/dist/backend/services/push-audience-evaluator.js.map +1 -0
- package/dist/backend/services/push-delivery-queue.d.ts +14 -0
- package/dist/backend/services/push-delivery-queue.d.ts.map +1 -0
- package/dist/backend/services/push-delivery-queue.js +30 -0
- package/dist/backend/services/push-delivery-queue.js.map +1 -0
- package/dist/backend/services/push-event-subscriber.d.ts +86 -0
- package/dist/backend/services/push-event-subscriber.d.ts.map +1 -0
- package/dist/backend/services/push-event-subscriber.js +109 -0
- package/dist/backend/services/push-event-subscriber.js.map +1 -0
- package/dist/backend/services/push-message-service.d.ts +58 -0
- package/dist/backend/services/push-message-service.d.ts.map +1 -0
- package/dist/backend/services/push-message-service.js +145 -0
- package/dist/backend/services/push-message-service.js.map +1 -0
- package/dist/backend/services/push-provider-registry.d.ts +17 -0
- package/dist/backend/services/push-provider-registry.d.ts.map +1 -0
- package/dist/backend/services/push-provider-registry.js +32 -0
- package/dist/backend/services/push-provider-registry.js.map +1 -0
- package/dist/backend/services/push-subscription-service.d.ts +67 -0
- package/dist/backend/services/push-subscription-service.d.ts.map +1 -0
- package/dist/backend/services/push-subscription-service.js +165 -0
- package/dist/backend/services/push-subscription-service.js.map +1 -0
- package/dist/backend/services/pwa-config-resolver.d.ts +29 -0
- package/dist/backend/services/pwa-config-resolver.d.ts.map +1 -0
- package/dist/backend/services/pwa-config-resolver.js +92 -0
- package/dist/backend/services/pwa-config-resolver.js.map +1 -0
- package/dist/backend/services/pwa-icon-service.d.ts +44 -0
- package/dist/backend/services/pwa-icon-service.d.ts.map +1 -0
- package/dist/backend/services/pwa-icon-service.js +105 -0
- package/dist/backend/services/pwa-icon-service.js.map +1 -0
- package/dist/backend/workers/push-delivery-worker.d.ts +17 -0
- package/dist/backend/workers/push-delivery-worker.d.ts.map +1 -0
- package/dist/backend/workers/push-delivery-worker.js +105 -0
- package/dist/backend/workers/push-delivery-worker.js.map +1 -0
- package/dist/manifest.d.ts +184 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +207 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260625T144228_pwa_init.d.ts +18 -0
- package/dist/migrations/20260625T144228_pwa_init.d.ts.map +1 -0
- package/dist/migrations/20260625T144228_pwa_init.js +105 -0
- package/dist/migrations/20260625T144228_pwa_init.js.map +1 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.d.ts +102 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.js +169 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.js.map +1 -0
- package/dist/migrations/index.d.ts +28 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +31 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/pwa.md +93 -0
- package/i18n/en.json +43 -0
- package/i18n/pl.json +43 -0
- package/package.json +105 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { AssetsLibraryPort, OrderReadPort } from '@endora-commerce/contracts';
|
|
2
|
+
import { type SalesChannelResolutionPort } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import type { PushEventTarget } from './push-event-subscriber.js';
|
|
4
|
+
/**
|
|
5
|
+
* Everything `pwa` needs from outside itself that is not one of its own rows
|
|
6
|
+
* (`specs/110-instance-repository/` T118c).
|
|
7
|
+
*
|
|
8
|
+
* It replaces `PwaBridge`, which was the same set written as closures in both
|
|
9
|
+
* composition roots — so a composition had to know how an asset id becomes a
|
|
10
|
+
* URL, how a channel id becomes a code and what an order-status push says, and
|
|
11
|
+
* the two roots disagreed about two of the three. What is left is
|
|
12
|
+
* **module-private** and holds three published ports and no closure, so there
|
|
13
|
+
* is nothing for a root to build and nothing for the two of them to differ on.
|
|
14
|
+
*
|
|
15
|
+
* `backend/index.ts` supplies each one as a `lazyPort` proxy, which resolves per
|
|
16
|
+
* call — nothing here captures a registration, so a switched-off owner refuses
|
|
17
|
+
* at the call rather than through a gate frozen at composition time.
|
|
18
|
+
*
|
|
19
|
+
* The functions below are functions **of the ports** rather than of a
|
|
20
|
+
* `ModuleContext`, which is what lets the mapping be tested over stubs with no
|
|
21
|
+
* container composed (`cross-module-context.test.ts`).
|
|
22
|
+
*/
|
|
23
|
+
export interface PwaCrossModulePorts {
|
|
24
|
+
/**
|
|
25
|
+
* `assets_library`, for the icon pipeline: the source and rendition uploads,
|
|
26
|
+
* and the URL the storefront icon route redirects to.
|
|
27
|
+
*/
|
|
28
|
+
readonly assets: AssetsLibraryPort;
|
|
29
|
+
/**
|
|
30
|
+
* The kernel's channel resolver — a platform name, not a module's, so it puts
|
|
31
|
+
* nothing in this module's manifest `dependencies` (Constitution XII).
|
|
32
|
+
*/
|
|
33
|
+
readonly channels: SalesChannelResolutionPort;
|
|
34
|
+
/** `orders`, for the FR-024 order-status auto-trigger. */
|
|
35
|
+
readonly orders: OrderReadPort;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* How a stored asset id becomes the URL the storefront icon route redirects to.
|
|
39
|
+
*
|
|
40
|
+
* **`getAsset`, not `resolveUrl`** (D-223): `assets_library` resolves the public
|
|
41
|
+
* origin itself and every URL it returns is absolute, so `resolveUrl` is
|
|
42
|
+
* published on no port and a consumer that wants a URL takes the detail that
|
|
43
|
+
* already carries one. Both roots used to call `resolveUrl` and one of them used
|
|
44
|
+
* to wrap it in an `absolutizePublicUrl` the other did not, so a push payload's
|
|
45
|
+
* icon URL depended on which root composed the platform.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createAssetUrlResolver(assets: Pick<AssetsLibraryPort, 'getAsset'>): (assetId: string) => Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* The channel a settings read or write falls back to when the admin surface
|
|
50
|
+
* names none: the deployment's system default.
|
|
51
|
+
*
|
|
52
|
+
* `null` is still in the return type and is still unreachable through this
|
|
53
|
+
* implementation — D-48 says exactly one row holds the flag on a booted
|
|
54
|
+
* deployment. The seam admits it because {@link PwaCrossModulePorts} is not the
|
|
55
|
+
* only thing that could ever answer, and because the option it feeds
|
|
56
|
+
* (`resolveScopeChannelId`) means "read platform-wide" by it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createDefaultChannelIdResolver(channels: Pick<SalesChannelResolutionPort, 'getSystemDefault'>): () => Promise<string | null>;
|
|
59
|
+
/**
|
|
60
|
+
* A channel id to its code, because a settings subset write is keyed by code.
|
|
61
|
+
*
|
|
62
|
+
* `getById` is the resolver's `em.findOne(SalesChannel, { id })` with a cache
|
|
63
|
+
* populate on the way out — the same read the composition roots wrote by hand,
|
|
64
|
+
* with the same absence of narrowing, so an inactive channel still answers its
|
|
65
|
+
* code and an unknown id still answers `null` (which the admin reset route
|
|
66
|
+
* turns into `PWA_CHANNEL_UNKNOWN`).
|
|
67
|
+
*/
|
|
68
|
+
export declare function createChannelCodeResolver(channels: Pick<SalesChannelResolutionPort, 'getById'>): (channelId: string) => Promise<string | null>;
|
|
69
|
+
/**
|
|
70
|
+
* What an `order.status_changed.v1` event becomes, as a push (FR-024).
|
|
71
|
+
*
|
|
72
|
+
* `null` means "no push": an order the platform will not read, and an order
|
|
73
|
+
* placed by nobody — a guest checkout has no account to notify, and there is no
|
|
74
|
+
* device to deliver to.
|
|
75
|
+
*
|
|
76
|
+
* The sentence is English and stays English: it is composed for a reader whose
|
|
77
|
+
* language this module cannot resolve here — one event, potentially many
|
|
78
|
+
* subscribed devices — which is case 2 of
|
|
79
|
+
* `specs/094-translation-boundary/contracts/translation-boundary.md` § 2 and is
|
|
80
|
+
* `specs/093-backend-delivered-prose/`'s subject rather than this drain's. It
|
|
81
|
+
* moved from a composition root unchanged, deliberately: a repair here would be
|
|
82
|
+
* a repair nobody asked for, in a merge request nobody would look for it in.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createOrderPushTargetResolver(orders: Pick<OrderReadPort, 'findById'>): (payload: {
|
|
85
|
+
orderId: string;
|
|
86
|
+
salesChannelId: string;
|
|
87
|
+
from: string;
|
|
88
|
+
to: string;
|
|
89
|
+
}) => Promise<PushEventTarget | null>;
|
|
90
|
+
//# sourceMappingURL=cross-module-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-module-context.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cross-module-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,GAC1C,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB7C;AAED;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,kBAAkB,CAAC,GAC7D,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,SAAS,CAAC,GACpD,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE/C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,GACtC,CAAC,OAAO,EAAE;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAYpC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { rethrowIfModuleDisabled, } from '@endora-commerce/platform/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* How a stored asset id becomes the URL the storefront icon route redirects to.
|
|
4
|
+
*
|
|
5
|
+
* **`getAsset`, not `resolveUrl`** (D-223): `assets_library` resolves the public
|
|
6
|
+
* origin itself and every URL it returns is absolute, so `resolveUrl` is
|
|
7
|
+
* published on no port and a consumer that wants a URL takes the detail that
|
|
8
|
+
* already carries one. Both roots used to call `resolveUrl` and one of them used
|
|
9
|
+
* to wrap it in an `absolutizePublicUrl` the other did not, so a push payload's
|
|
10
|
+
* icon URL depended on which root composed the platform.
|
|
11
|
+
*/
|
|
12
|
+
export function createAssetUrlResolver(assets) {
|
|
13
|
+
return async (assetId) => {
|
|
14
|
+
try {
|
|
15
|
+
return (await assets.getAsset(assetId)).url;
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
// The narrow tolerance both composition roots already carried, kept
|
|
19
|
+
// deliberately: `getAsset` throws 404 for a row that is gone, and an icon
|
|
20
|
+
// rendition pointing at a deleted asset answers 404 on that one size
|
|
21
|
+
// rather than 500. `rethrowIfModuleDisabled` is what stops the tolerance
|
|
22
|
+
// from also absorbing the owner's refusal (composition checklist item 7)
|
|
23
|
+
// — unreachable while `assets_library` declares `nonDeactivatable`, and
|
|
24
|
+
// the line that keeps this fail-closed on the day that changes.
|
|
25
|
+
rethrowIfModuleDisabled(err);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The channel a settings read or write falls back to when the admin surface
|
|
32
|
+
* names none: the deployment's system default.
|
|
33
|
+
*
|
|
34
|
+
* `null` is still in the return type and is still unreachable through this
|
|
35
|
+
* implementation — D-48 says exactly one row holds the flag on a booted
|
|
36
|
+
* deployment. The seam admits it because {@link PwaCrossModulePorts} is not the
|
|
37
|
+
* only thing that could ever answer, and because the option it feeds
|
|
38
|
+
* (`resolveScopeChannelId`) means "read platform-wide" by it.
|
|
39
|
+
*/
|
|
40
|
+
export function createDefaultChannelIdResolver(channels) {
|
|
41
|
+
return async () => (await channels.getSystemDefault()).id;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A channel id to its code, because a settings subset write is keyed by code.
|
|
45
|
+
*
|
|
46
|
+
* `getById` is the resolver's `em.findOne(SalesChannel, { id })` with a cache
|
|
47
|
+
* populate on the way out — the same read the composition roots wrote by hand,
|
|
48
|
+
* with the same absence of narrowing, so an inactive channel still answers its
|
|
49
|
+
* code and an unknown id still answers `null` (which the admin reset route
|
|
50
|
+
* turns into `PWA_CHANNEL_UNKNOWN`).
|
|
51
|
+
*/
|
|
52
|
+
export function createChannelCodeResolver(channels) {
|
|
53
|
+
return async (channelId) => (await channels.getById(channelId))?.code ?? null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What an `order.status_changed.v1` event becomes, as a push (FR-024).
|
|
57
|
+
*
|
|
58
|
+
* `null` means "no push": an order the platform will not read, and an order
|
|
59
|
+
* placed by nobody — a guest checkout has no account to notify, and there is no
|
|
60
|
+
* device to deliver to.
|
|
61
|
+
*
|
|
62
|
+
* The sentence is English and stays English: it is composed for a reader whose
|
|
63
|
+
* language this module cannot resolve here — one event, potentially many
|
|
64
|
+
* subscribed devices — which is case 2 of
|
|
65
|
+
* `specs/094-translation-boundary/contracts/translation-boundary.md` § 2 and is
|
|
66
|
+
* `specs/093-backend-delivered-prose/`'s subject rather than this drain's. It
|
|
67
|
+
* moved from a composition root unchanged, deliberately: a repair here would be
|
|
68
|
+
* a repair nobody asked for, in a merge request nobody would look for it in.
|
|
69
|
+
*/
|
|
70
|
+
export function createOrderPushTargetResolver(orders) {
|
|
71
|
+
return async (payload) => {
|
|
72
|
+
const order = await orders.findById(payload.orderId);
|
|
73
|
+
if (!order || !order.placedByCustomerAccountId)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
salesChannelId: payload.salesChannelId,
|
|
77
|
+
customerAccountId: order.placedByCustomerAccountId,
|
|
78
|
+
title: 'Order update',
|
|
79
|
+
body: `Order ${order.businessId} is now ${payload.to.replace(/_/g, ' ')}.`,
|
|
80
|
+
url: `/account/orders/${order.businessId}`,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=cross-module-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-module-context.js","sourceRoot":"","sources":["../../../src/backend/services/cross-module-context.ts"],"names":[],"mappings":"AACA,OAAO,EACL,uBAAuB,GAExB,MAAM,kCAAkC,CAAC;AAqC1C;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAA2C;IAE3C,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,oEAAoE;YACpE,0EAA0E;YAC1E,qEAAqE;YACrE,yEAAyE;YACzE,yEAAyE;YACzE,wEAAwE;YACxE,gEAAgE;YAChE,uBAAuB,CAAC,GAAG,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAA8D;IAE9D,OAAO,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CACvC,QAAqD;IAErD,OAAO,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,6BAA6B,CAC3C,MAAuC;IAOvC,OAAO,KAAK,EAAE,OAAO,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,yBAAyB;YAAE,OAAO,IAAI,CAAC;QAC5D,OAAO;YACL,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,iBAAiB,EAAE,KAAK,CAAC,yBAAyB;YAClD,KAAK,EAAE,cAAc;YACrB,IAAI,EAAE,SAAS,KAAK,CAAC,UAAU,WAAW,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG;YAC1E,GAAG,EAAE,mBAAmB,KAAK,CAAC,UAAU,EAAE;SAC3C,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type PushPayload, type PushProvider, type PushSendResult, type PushSubscriptionRef } from '@endora-commerce/contracts';
|
|
3
|
+
export interface WebPushSettingsPort {
|
|
4
|
+
get<T>(code: string, salesChannelId: string, schema: z.ZodType<T>): Promise<T>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Default push provider (FR-019): standard Web-Push signed with VAPID via the
|
|
8
|
+
* `web-push` library. On Chrome/Edge the endpoints are transported by Google's
|
|
9
|
+
* free FCM, so this path is "FCM as backend" while keeping the storefront on
|
|
10
|
+
* standard browser APIs.
|
|
11
|
+
*
|
|
12
|
+
* Stateless and concurrency-safe: VAPID keys are resolved per call from the
|
|
13
|
+
* subscription's channel and passed via `sendNotification`'s `vapidDetails`
|
|
14
|
+
* option (never `setVapidDetails`, which is process-global).
|
|
15
|
+
*
|
|
16
|
+
* Status mapping (research §R1):
|
|
17
|
+
* 2xx → ok
|
|
18
|
+
* 404 / 410 → gone (caller prunes the subscription, FR-022)
|
|
19
|
+
* 429 / 5xx → retryable
|
|
20
|
+
* other 4xx → non-retryable failure
|
|
21
|
+
*/
|
|
22
|
+
export declare class WebPushProvider implements PushProvider {
|
|
23
|
+
private readonly settings;
|
|
24
|
+
private readonly subject;
|
|
25
|
+
readonly key = "web_push";
|
|
26
|
+
constructor(settings: WebPushSettingsPort, subject: string);
|
|
27
|
+
private resolveKeys;
|
|
28
|
+
isConfigured(salesChannelId: string): Promise<boolean>;
|
|
29
|
+
send(sub: PushSubscriptionRef, payload: PushPayload): Promise<PushSendResult>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=web-push-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-push-provider.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/providers/web-push-provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAChF;AAID;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,eAAgB,YAAW,YAAY;IAIhD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,QAAQ,CAAC,GAAG,cAAc;gBAGP,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,MAAM;YAGpB,WAAW;IAenB,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItD,IAAI,CAAC,GAAG,EAAE,mBAAmB,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC;CA+BpF"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import webpush from 'web-push';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { PWA_SETTING_CODES, } from '@endora-commerce/contracts';
|
|
4
|
+
const StringSchema = z.string();
|
|
5
|
+
/**
|
|
6
|
+
* Default push provider (FR-019): standard Web-Push signed with VAPID via the
|
|
7
|
+
* `web-push` library. On Chrome/Edge the endpoints are transported by Google's
|
|
8
|
+
* free FCM, so this path is "FCM as backend" while keeping the storefront on
|
|
9
|
+
* standard browser APIs.
|
|
10
|
+
*
|
|
11
|
+
* Stateless and concurrency-safe: VAPID keys are resolved per call from the
|
|
12
|
+
* subscription's channel and passed via `sendNotification`'s `vapidDetails`
|
|
13
|
+
* option (never `setVapidDetails`, which is process-global).
|
|
14
|
+
*
|
|
15
|
+
* Status mapping (research §R1):
|
|
16
|
+
* 2xx → ok
|
|
17
|
+
* 404 / 410 → gone (caller prunes the subscription, FR-022)
|
|
18
|
+
* 429 / 5xx → retryable
|
|
19
|
+
* other 4xx → non-retryable failure
|
|
20
|
+
*/
|
|
21
|
+
export class WebPushProvider {
|
|
22
|
+
settings;
|
|
23
|
+
subject;
|
|
24
|
+
key = 'web_push';
|
|
25
|
+
constructor(settings, subject) {
|
|
26
|
+
this.settings = settings;
|
|
27
|
+
this.subject = subject;
|
|
28
|
+
}
|
|
29
|
+
async resolveKeys(salesChannelId) {
|
|
30
|
+
try {
|
|
31
|
+
const [publicKey, privateKey] = await Promise.all([
|
|
32
|
+
this.settings.get(PWA_SETTING_CODES.VAPID_PUBLIC_KEY, salesChannelId, StringSchema),
|
|
33
|
+
this.settings.get(PWA_SETTING_CODES.VAPID_PRIVATE_KEY, salesChannelId, StringSchema),
|
|
34
|
+
]);
|
|
35
|
+
if (!publicKey || !privateKey)
|
|
36
|
+
return null;
|
|
37
|
+
return { publicKey, privateKey };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async isConfigured(salesChannelId) {
|
|
44
|
+
return (await this.resolveKeys(salesChannelId)) !== null;
|
|
45
|
+
}
|
|
46
|
+
async send(sub, payload) {
|
|
47
|
+
const keys = await this.resolveKeys(sub.salesChannelId);
|
|
48
|
+
if (!keys) {
|
|
49
|
+
return { ok: false, gone: false, error: 'vapid_unconfigured', retryable: false };
|
|
50
|
+
}
|
|
51
|
+
const body = JSON.stringify({
|
|
52
|
+
title: payload.title,
|
|
53
|
+
body: payload.body,
|
|
54
|
+
icon: payload.iconUrl,
|
|
55
|
+
url: payload.url,
|
|
56
|
+
tag: payload.tag,
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
await webpush.sendNotification({ endpoint: sub.endpoint, keys: { p256dh: sub.keys.p256dh, auth: sub.keys.auth } }, body, { vapidDetails: { subject: this.subject, publicKey: keys.publicKey, privateKey: keys.privateKey } });
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const statusCode = err.statusCode ?? 0;
|
|
64
|
+
if (statusCode === 404 || statusCode === 410) {
|
|
65
|
+
return { ok: false, gone: true };
|
|
66
|
+
}
|
|
67
|
+
const retryable = statusCode === 429 || (statusCode >= 500 && statusCode < 600) || statusCode === 0;
|
|
68
|
+
return { ok: false, gone: false, error: `web-push status ${statusCode}`, retryable };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=web-push-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-push-provider.js","sourceRoot":"","sources":["../../../../src/backend/services/providers/web-push-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PushAudienceRule } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Pure push-audience rule evaluator (Rule Builder targeting).
|
|
4
|
+
*
|
|
5
|
+
* Walks the AST defined in `@endora-commerce/contracts/pwa#PushAudienceRule` and returns
|
|
6
|
+
* whether a single subscriber (resolved into the context below) is in the
|
|
7
|
+
* target audience. Mirrors the price-list `evaluateApplicationRule` shape but
|
|
8
|
+
* keeps a boolean result — push targeting has no priority chain.
|
|
9
|
+
*
|
|
10
|
+
* Semantics:
|
|
11
|
+
* - `{ kind: 'all' }` and any criterion with an empty value list match
|
|
12
|
+
* everyone (an empty criterion adds no constraint).
|
|
13
|
+
* - Anonymous subscribers (`customerAccountId === null`) carry null
|
|
14
|
+
* organization/customer-group/customer context, so only `salesChannel`
|
|
15
|
+
* criteria (and `all`) can match them.
|
|
16
|
+
*/
|
|
17
|
+
export interface PushAudienceContext {
|
|
18
|
+
salesChannelId: string;
|
|
19
|
+
organizationId: string | null;
|
|
20
|
+
customerGroupId: string | null;
|
|
21
|
+
customerAccountId: string | null;
|
|
22
|
+
}
|
|
23
|
+
export declare function evaluatePushAudienceRule(rule: PushAudienceRule, ctx: PushAudienceContext): boolean;
|
|
24
|
+
//# sourceMappingURL=push-audience-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-audience-evaluator.d.ts","sourceRoot":"","sources":["../../../src/backend/services/push-audience-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9F;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,gBAAgB,EACtB,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAcT"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function evaluatePushAudienceRule(rule, ctx) {
|
|
2
|
+
if (rule.kind === 'all')
|
|
3
|
+
return true;
|
|
4
|
+
if (rule.kind === 'criterion') {
|
|
5
|
+
if (rule.values.length === 0)
|
|
6
|
+
return true;
|
|
7
|
+
return matchesCriterion(rule.type, rule.values, ctx);
|
|
8
|
+
}
|
|
9
|
+
// group node
|
|
10
|
+
if (rule.children.length === 0)
|
|
11
|
+
return true; // defensive — schema disallows
|
|
12
|
+
if (rule.op === 'AND') {
|
|
13
|
+
return rule.children.every((c) => evaluatePushAudienceRule(c, ctx));
|
|
14
|
+
}
|
|
15
|
+
return rule.children.some((c) => evaluatePushAudienceRule(c, ctx));
|
|
16
|
+
}
|
|
17
|
+
function matchesCriterion(type, values, ctx) {
|
|
18
|
+
switch (type) {
|
|
19
|
+
case 'salesChannel':
|
|
20
|
+
return values.includes(ctx.salesChannelId);
|
|
21
|
+
case 'organization':
|
|
22
|
+
return ctx.organizationId !== null && values.includes(ctx.organizationId);
|
|
23
|
+
case 'customerGroup':
|
|
24
|
+
return ctx.customerGroupId !== null && values.includes(ctx.customerGroupId);
|
|
25
|
+
case 'customer':
|
|
26
|
+
return ctx.customerAccountId !== null && values.includes(ctx.customerAccountId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=push-audience-evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-audience-evaluator.js","sourceRoot":"","sources":["../../../src/backend/services/push-audience-evaluator.ts"],"names":[],"mappings":"AAwBA,MAAM,UAAU,wBAAwB,CACtC,IAAsB,EACtB,GAAwB;IAExB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAErC,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC1C,OAAO,gBAAgB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvD,CAAC;IAED,aAAa;IACb,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,+BAA+B;IAC5E,IAAI,IAAI,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,gBAAgB,CACvB,IAA+B,EAC/B,MAAyB,EACzB,GAAwB;IAExB,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc;YACjB,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC7C,KAAK,cAAc;YACjB,OAAO,GAAG,CAAC,cAAc,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC5E,KAAK,eAAe;YAClB,OAAO,GAAG,CAAC,eAAe,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAC9E,KAAK,UAAU;YACb,OAAO,GAAG,CAAC,iBAAiB,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACpF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Queue, Worker, type Processor, type QueueOptions, type WorkerOptions } from 'bullmq';
|
|
2
|
+
import type { Redis } from 'ioredis';
|
|
3
|
+
/**
|
|
4
|
+
* BullMQ queue for push delivery fan-out (Principle X). One job per
|
|
5
|
+
* `push_message_deliveries` row; the unique (message_id, subscription_id)
|
|
6
|
+
* delivery row makes redelivery idempotent.
|
|
7
|
+
*/
|
|
8
|
+
export interface PushDeliveryJobData {
|
|
9
|
+
deliveryId: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const PUSH_DELIVERY_QUEUE_NAME = "pwa.push.deliver";
|
|
12
|
+
export declare function createPushDeliveryQueue(redis: Redis, overrides?: Partial<QueueOptions>): Queue<PushDeliveryJobData>;
|
|
13
|
+
export declare function createPushDeliveryWorker(redis: Redis, processor: Processor<PushDeliveryJobData>, overrides?: Partial<WorkerOptions>): Worker<PushDeliveryJobData>;
|
|
14
|
+
//# sourceMappingURL=push-delivery-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-delivery-queue.d.ts","sourceRoot":"","sources":["../../../src/backend/services/push-delivery-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE9F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAK3D,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,KAAK,EACZ,SAAS,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAChC,KAAK,CAAC,mBAAmB,CAAC,CAY5B;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC,EACzC,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GACjC,MAAM,CAAC,mBAAmB,CAAC,CAc7B"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Queue, Worker } from 'bullmq';
|
|
2
|
+
import { enterSystemScope } from '@endora-commerce/platform/kernel';
|
|
3
|
+
export const PUSH_DELIVERY_QUEUE_NAME = 'pwa.push.deliver';
|
|
4
|
+
const DEFAULT_ATTEMPTS = 5;
|
|
5
|
+
const DEFAULT_BACKOFF = { type: 'exponential', delay: 2_000 };
|
|
6
|
+
export function createPushDeliveryQueue(redis, overrides) {
|
|
7
|
+
const options = {
|
|
8
|
+
connection: redis,
|
|
9
|
+
defaultJobOptions: {
|
|
10
|
+
attempts: DEFAULT_ATTEMPTS,
|
|
11
|
+
backoff: DEFAULT_BACKOFF,
|
|
12
|
+
removeOnComplete: { count: 1_000 },
|
|
13
|
+
removeOnFail: { count: 5_000 },
|
|
14
|
+
},
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
return new Queue(PUSH_DELIVERY_QUEUE_NAME, options);
|
|
18
|
+
}
|
|
19
|
+
export function createPushDeliveryWorker(redis, processor, overrides) {
|
|
20
|
+
// Feature 072 (T033) — the scope moves here from inside the processor, so it
|
|
21
|
+
// sits at the entry point like every other queue in the tree; a processor
|
|
22
|
+
// called directly (a test, a future in-process caller) is no longer the only
|
|
23
|
+
// thing that decides whether a context exists.
|
|
24
|
+
return new Worker(PUSH_DELIVERY_QUEUE_NAME, (job) => enterSystemScope('pwa: push delivery', () => processor(job)), {
|
|
25
|
+
connection: redis,
|
|
26
|
+
concurrency: 8,
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=push-delivery-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-delivery-queue.js","sourceRoot":"","sources":["../../../src/backend/services/push-delivery-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAyD,MAAM,QAAQ,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAYpE,MAAM,CAAC,MAAM,wBAAwB,GAAG,kBAAkB,CAAC;AAE3D,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,eAAe,GAAG,EAAE,IAAI,EAAE,aAAsB,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAEvE,MAAM,UAAU,uBAAuB,CACrC,KAAY,EACZ,SAAiC;IAEjC,MAAM,OAAO,GAAiB;QAC5B,UAAU,EAAE,KAAK;QACjB,iBAAiB,EAAE;YACjB,QAAQ,EAAE,gBAAgB;YAC1B,OAAO,EAAE,eAAe;YACxB,gBAAgB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;YAClC,YAAY,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;SAC/B;QACD,GAAG,SAAS;KACb,CAAC;IACF,OAAO,IAAI,KAAK,CAAsB,wBAAwB,EAAE,OAAO,CAAC,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,KAAY,EACZ,SAAyC,EACzC,SAAkC;IAElC,6EAA6E;IAC7E,0EAA0E;IAC1E,6EAA6E;IAC7E,+CAA+C;IAC/C,OAAO,IAAI,MAAM,CACf,wBAAwB,EACxB,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EACrE;QACE,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,CAAC;QACd,GAAG,SAAS;KACb,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type PlatformLogger } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import type { PushMessageService } from './push-message-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* Where a skipped or failed auto-trigger push is written down.
|
|
5
|
+
*
|
|
6
|
+
* The structured-logger surface the kernel already defines, for the reason it
|
|
7
|
+
* exists: these two handlers run outside any request, so `request.log` does not
|
|
8
|
+
* exist and the line has to carry its own context. The alias is kept because it
|
|
9
|
+
* names this module's role for the field; the shape is the platform's.
|
|
10
|
+
*/
|
|
11
|
+
export type PushEventLogger = PlatformLogger;
|
|
12
|
+
/**
|
|
13
|
+
* Target resolved for an auto-triggered push. Returning null (or no customer)
|
|
14
|
+
* means "no push" — anonymous orders / unsubscribed customers are skipped.
|
|
15
|
+
*/
|
|
16
|
+
export interface PushEventTarget {
|
|
17
|
+
salesChannelId: string;
|
|
18
|
+
customerAccountId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
body: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface PushEventSubscriberDeps {
|
|
24
|
+
messageService: PushMessageService;
|
|
25
|
+
/** Resolve an order-status event into a push target (channel + customer + copy). */
|
|
26
|
+
resolveOrderTarget?: (payload: {
|
|
27
|
+
orderId: string;
|
|
28
|
+
salesChannelId: string;
|
|
29
|
+
from: string;
|
|
30
|
+
to: string;
|
|
31
|
+
}) => Promise<PushEventTarget | null>;
|
|
32
|
+
/** Resolve a quote-request event into a push target. */
|
|
33
|
+
resolveQuoteTarget?: (payload: {
|
|
34
|
+
quoteRequestId: string;
|
|
35
|
+
sourceEventId: string;
|
|
36
|
+
}) => Promise<PushEventTarget | null>;
|
|
37
|
+
/** Gate: is push enabled for this channel? */
|
|
38
|
+
isPushEnabled: (salesChannelId: string) => Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Read per line rather than captured, because the logger these handlers
|
|
41
|
+
* should be writing to does not exist yet when they are built: composition
|
|
42
|
+
* runs before `buildServer`, so `ctx.log` is whatever the root could offer
|
|
43
|
+
* that early, and the application's own logger only arrives when the module's
|
|
44
|
+
* routes are registered. `plugin.ts` moves the holder along; the handlers ask
|
|
45
|
+
* for it at the moment they have something to say.
|
|
46
|
+
*/
|
|
47
|
+
log: () => PushEventLogger;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The two auto-trigger handlers (FR-024). One push message per event,
|
|
51
|
+
* idempotent on the event id; producer only — nothing is sent inline
|
|
52
|
+
* (Principle X), and every handler absorbs its own failure so a push problem
|
|
53
|
+
* never breaks the business transaction that announced itself.
|
|
54
|
+
*
|
|
55
|
+
* The subscriptions live in this module's `backend.ts` and go through
|
|
56
|
+
* `ctx.subscribe` (issue #107). They were two bare `eventBus.on` calls here, so
|
|
57
|
+
* a switched-off `pwa` still wrote a `push_messages` row and still delivered a
|
|
58
|
+
* notification to a customer's device — the most visible of the writes that
|
|
59
|
+
* survived their module.
|
|
60
|
+
*
|
|
61
|
+
* ## Why the `catch` stays, and what it is no longer allowed to hide
|
|
62
|
+
*
|
|
63
|
+
* `PushMessageService` reaches `customer_accounts` and `organizations` through
|
|
64
|
+
* gated ports (feature 075, Phase C), so a rule-targeted send can raise
|
|
65
|
+
* `ModuleDisabledError` here. `rethrowIfModuleDisabled` — the usual remedy —
|
|
66
|
+
* is the wrong one at this seam: an EventBus subscriber has no caller to answer.
|
|
67
|
+
* `EventBus.dispatch` isolates each handler in its own `try` and turns a throw
|
|
68
|
+
* into one `console.warn` line naming neither the module nor the trigger, so
|
|
69
|
+
* re-throwing would hand the presence answer to a place that discards it, and a
|
|
70
|
+
* probe before the work would make `pwa` name the two owners the port exists to
|
|
71
|
+
* hide.
|
|
72
|
+
*
|
|
73
|
+
* So the presence answer is **decided here, by name** — the third of the four
|
|
74
|
+
* shapes `check-port-catches` accepts — and it is decided into a different
|
|
75
|
+
* sentence from a failure. The outcome is the same for both (no push, the
|
|
76
|
+
* announcing transaction untouched, per FR-024); the record is not, because an
|
|
77
|
+
* operator's own switch doing what it says and a broken enqueue are not one
|
|
78
|
+
* event. That is the rule under Principle XVII item 3: where nothing can catch
|
|
79
|
+
* the throw, a genuine failure and a switched-off module must not share one
|
|
80
|
+
* silent no-op.
|
|
81
|
+
*/
|
|
82
|
+
export declare function createPushEventHandlers(deps: PushEventSubscriberDeps): {
|
|
83
|
+
onOrderStatusChanged: (payload: unknown) => Promise<void>;
|
|
84
|
+
onQuoteRequestUpdated: (payload: unknown) => Promise<void>;
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=push-event-subscriber.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-event-subscriber.d.ts","sourceRoot":"","sources":["../../../src/backend/services/push-event-subscriber.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,kBAAkB,CAAC;IACnC,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC7B,OAAO,EAAE,MAAM,CAAC;QAChB,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;KACZ,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACtC,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC7B,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;KACvB,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACtC,8CAA8C;IAC9C,aAAa,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,eAAe,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,uBAAuB,GAAG;IACtE,oBAAoB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,qBAAqB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D,CAyFA"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ModuleDisabledError, } from '@endora-commerce/platform/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* The two auto-trigger handlers (FR-024). One push message per event,
|
|
4
|
+
* idempotent on the event id; producer only — nothing is sent inline
|
|
5
|
+
* (Principle X), and every handler absorbs its own failure so a push problem
|
|
6
|
+
* never breaks the business transaction that announced itself.
|
|
7
|
+
*
|
|
8
|
+
* The subscriptions live in this module's `backend.ts` and go through
|
|
9
|
+
* `ctx.subscribe` (issue #107). They were two bare `eventBus.on` calls here, so
|
|
10
|
+
* a switched-off `pwa` still wrote a `push_messages` row and still delivered a
|
|
11
|
+
* notification to a customer's device — the most visible of the writes that
|
|
12
|
+
* survived their module.
|
|
13
|
+
*
|
|
14
|
+
* ## Why the `catch` stays, and what it is no longer allowed to hide
|
|
15
|
+
*
|
|
16
|
+
* `PushMessageService` reaches `customer_accounts` and `organizations` through
|
|
17
|
+
* gated ports (feature 075, Phase C), so a rule-targeted send can raise
|
|
18
|
+
* `ModuleDisabledError` here. `rethrowIfModuleDisabled` — the usual remedy —
|
|
19
|
+
* is the wrong one at this seam: an EventBus subscriber has no caller to answer.
|
|
20
|
+
* `EventBus.dispatch` isolates each handler in its own `try` and turns a throw
|
|
21
|
+
* into one `console.warn` line naming neither the module nor the trigger, so
|
|
22
|
+
* re-throwing would hand the presence answer to a place that discards it, and a
|
|
23
|
+
* probe before the work would make `pwa` name the two owners the port exists to
|
|
24
|
+
* hide.
|
|
25
|
+
*
|
|
26
|
+
* So the presence answer is **decided here, by name** — the third of the four
|
|
27
|
+
* shapes `check-port-catches` accepts — and it is decided into a different
|
|
28
|
+
* sentence from a failure. The outcome is the same for both (no push, the
|
|
29
|
+
* announcing transaction untouched, per FR-024); the record is not, because an
|
|
30
|
+
* operator's own switch doing what it says and a broken enqueue are not one
|
|
31
|
+
* event. That is the rule under Principle XVII item 3: where nothing can catch
|
|
32
|
+
* the throw, a genuine failure and a switched-off module must not share one
|
|
33
|
+
* silent no-op.
|
|
34
|
+
*/
|
|
35
|
+
export function createPushEventHandlers(deps) {
|
|
36
|
+
const { messageService } = deps;
|
|
37
|
+
return {
|
|
38
|
+
// order.status_changed.v1 (orders, feature 038)
|
|
39
|
+
async onOrderStatusChanged(raw) {
|
|
40
|
+
const payload = raw;
|
|
41
|
+
try {
|
|
42
|
+
if (!deps.resolveOrderTarget)
|
|
43
|
+
return;
|
|
44
|
+
if (!(await deps.isPushEnabled(payload.salesChannelId)))
|
|
45
|
+
return;
|
|
46
|
+
const target = await deps.resolveOrderTarget(payload);
|
|
47
|
+
if (!target)
|
|
48
|
+
return;
|
|
49
|
+
await messageService.createAndEnqueue({
|
|
50
|
+
salesChannelId: target.salesChannelId,
|
|
51
|
+
title: target.title,
|
|
52
|
+
body: target.body,
|
|
53
|
+
...(target.url ? { url: target.url } : {}),
|
|
54
|
+
audience: { kind: 'customers', customerAccountIds: [target.customerAccountId] },
|
|
55
|
+
trigger: 'order_status',
|
|
56
|
+
sourceEventId: payload.eventId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Decided at the site, not delegated: the identifier below is the whole
|
|
61
|
+
// of what tells an operator's switch from an incident, and a helper
|
|
62
|
+
// holding it would let a later edit lose the distinction silently.
|
|
63
|
+
if (err instanceof ModuleDisabledError) {
|
|
64
|
+
deps.log().warn({ module: err.moduleId, trigger: 'order_status', eventId: payload.eventId }, 'pwa.push_skipped_module_disabled');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
deps
|
|
68
|
+
.log()
|
|
69
|
+
.error({ err, trigger: 'order_status', eventId: payload.eventId }, 'pwa.push_enqueue_failed');
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
// quote-request update events (quote_requests, feature 008)
|
|
73
|
+
async onQuoteRequestUpdated(raw) {
|
|
74
|
+
const payload = raw;
|
|
75
|
+
try {
|
|
76
|
+
if (!deps.resolveQuoteTarget)
|
|
77
|
+
return;
|
|
78
|
+
const target = await deps.resolveQuoteTarget({
|
|
79
|
+
quoteRequestId: payload.quoteRequestId,
|
|
80
|
+
sourceEventId: payload.eventId,
|
|
81
|
+
});
|
|
82
|
+
if (!target)
|
|
83
|
+
return;
|
|
84
|
+
if (!(await deps.isPushEnabled(target.salesChannelId)))
|
|
85
|
+
return;
|
|
86
|
+
await messageService.createAndEnqueue({
|
|
87
|
+
salesChannelId: target.salesChannelId,
|
|
88
|
+
title: target.title,
|
|
89
|
+
body: target.body,
|
|
90
|
+
...(target.url ? { url: target.url } : {}),
|
|
91
|
+
audience: { kind: 'customers', customerAccountIds: [target.customerAccountId] },
|
|
92
|
+
trigger: 'quote_request',
|
|
93
|
+
sourceEventId: payload.eventId,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// See the order-status twin above — same decision, same two sentences.
|
|
98
|
+
if (err instanceof ModuleDisabledError) {
|
|
99
|
+
deps.log().warn({ module: err.moduleId, trigger: 'quote_request', eventId: payload.eventId }, 'pwa.push_skipped_module_disabled');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
deps
|
|
103
|
+
.log()
|
|
104
|
+
.error({ err, trigger: 'quote_request', eventId: payload.eventId }, 'pwa.push_enqueue_failed');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=push-event-subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-event-subscriber.js","sourceRoot":"","sources":["../../../src/backend/services/push-event-subscriber.ts"],"names":[],"mappings":"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"}
|