@aglyn/plugins-commerce 1.0.0-beta.218 → 1.0.0-beta.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +6 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -19
  5. package/src/index.js.map +1 -1
  6. package/src/lib/components/cart.js +2 -1
  7. package/src/lib/components/cart.js.map +1 -1
  8. package/src/lib/components/console/catalog-organization-card.component.js +665 -459
  9. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-orders-card.component.js +2 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/member-posts-card.component.js +1 -1
  13. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  14. package/src/lib/components/console/order-detail-dialog.component.js +8 -7
  15. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  16. package/src/lib/components/console/payments-settings-card.component.js +2 -2
  17. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  18. package/src/lib/components/console/product-editor-dialog.component.js +1221 -889
  19. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/product-zones.d.ts +184 -0
  21. package/src/lib/components/console/product-zones.js +54 -0
  22. package/src/lib/components/console/product-zones.js.map +1 -0
  23. package/src/lib/components/console/products-hub-card.component.js +3 -2
  24. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.d.ts +1 -1
  26. package/src/lib/components/console/products-hub-zone.component.js +2 -1
  27. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  28. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  29. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  30. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  31. package/src/lib/components/console/store-settings-card.component.js +2 -2
  32. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  33. package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -1
  34. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
  35. package/src/lib/components/member-signup.js +2 -2
  36. package/src/lib/components/member-signup.js.map +1 -1
  37. package/src/lib/components/newsletter-signup.js +2 -2
  38. package/src/lib/components/newsletter-signup.js.map +1 -1
  39. package/src/lib/components/product-detail.js +2 -1
  40. package/src/lib/components/product-detail.js.map +1 -1
  41. package/src/lib/components/product-grid.js +1 -1
  42. package/src/lib/components/product-grid.js.map +1 -1
  43. package/src/lib/constants/operator-alerts.js +5 -3
  44. package/src/lib/constants/operator-alerts.js.map +1 -1
  45. package/src/lib/constants/storefront-payments.d.ts +33 -0
  46. package/src/lib/constants/storefront-payments.js +35 -0
  47. package/src/lib/constants/storefront-payments.js.map +1 -0
  48. package/src/lib/declarations.server.d.ts +3 -1
  49. package/src/lib/declarations.server.js +19 -1
  50. package/src/lib/declarations.server.js.map +1 -1
  51. package/src/lib/model/commerce-io.js +1 -1
  52. package/src/lib/model/commerce-io.js.map +1 -1
  53. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  54. package/src/lib/model/commerce-record-lists.js +45 -0
  55. package/src/lib/model/commerce-record-lists.js.map +1 -0
  56. package/src/lib/model/commerce-record-routes.d.ts +45 -0
  57. package/src/lib/model/commerce-record-routes.js +72 -0
  58. package/src/lib/model/commerce-record-routes.js.map +1 -0
  59. package/src/lib/model/commerce-search-sources.d.ts +21 -0
  60. package/src/lib/model/commerce-search-sources.js +42 -0
  61. package/src/lib/model/commerce-search-sources.js.map +1 -0
  62. package/src/lib/model/commerce-topics.d.ts +20 -0
  63. package/src/lib/model/commerce-topics.js +33 -0
  64. package/src/lib/model/commerce-topics.js.map +1 -0
  65. package/src/lib/model/index.d.ts +1 -0
  66. package/src/lib/model/index.js +1 -0
  67. package/src/lib/model/index.js.map +1 -1
  68. package/src/lib/model/product-record.d.ts +32 -0
  69. package/src/lib/model/product-record.js +82 -0
  70. package/src/lib/model/product-record.js.map +1 -0
  71. package/src/lib/model/site-member-purchases.d.ts +48 -0
  72. package/src/lib/model/site-member-purchases.js +50 -0
  73. package/src/lib/model/site-member-purchases.js.map +1 -0
  74. package/src/lib/model/storefront-tax-summary.d.ts +121 -0
  75. package/src/lib/model/storefront-tax-summary.js +148 -0
  76. package/src/lib/model/storefront-tax-summary.js.map +1 -0
  77. package/src/lib/plan-entitlements.d.ts +35 -0
  78. package/src/lib/plan-entitlements.js +54 -0
  79. package/src/lib/plan-entitlements.js.map +1 -0
  80. package/src/lib/plugin.js +21 -0
  81. package/src/lib/plugin.js.map +1 -1
  82. package/src/lib/server/billing-webhook.js +387 -248
  83. package/src/lib/server/billing-webhook.js.map +1 -1
  84. package/src/lib/server/cart-checkout.js +3 -3
  85. package/src/lib/server/cart-checkout.js.map +1 -1
  86. package/src/lib/server/checkout.js +2 -2
  87. package/src/lib/server/checkout.js.map +1 -1
  88. package/src/lib/server/connect.js +5 -5
  89. package/src/lib/server/connect.js.map +1 -1
  90. package/src/lib/server/draft-order.js +2 -2
  91. package/src/lib/server/draft-order.js.map +1 -1
  92. package/src/lib/server/gift-cards.js +7 -3
  93. package/src/lib/server/gift-cards.js.map +1 -1
  94. package/src/lib/server/low-stock.js +3 -1
  95. package/src/lib/server/low-stock.js.map +1 -1
  96. package/src/lib/server/member-post.js +2 -2
  97. package/src/lib/server/member-post.js.map +1 -1
  98. package/src/lib/server/membership-admin-password.js +4 -1
  99. package/src/lib/server/membership-admin-password.js.map +1 -1
  100. package/src/lib/server/membership-content.js +1 -1
  101. package/src/lib/server/membership-content.js.map +1 -1
  102. package/src/lib/server/membership-register.js +13 -2
  103. package/src/lib/server/membership-register.js.map +1 -1
  104. package/src/lib/server/newsletter.js +32 -19
  105. package/src/lib/server/newsletter.js.map +1 -1
  106. package/src/lib/server/pos-order.js +3 -3
  107. package/src/lib/server/pos-order.js.map +1 -1
  108. package/src/lib/server/process-abandoned.js +4 -2
  109. package/src/lib/server/process-abandoned.js.map +1 -1
  110. package/src/lib/server/process-restock.js +7 -5
  111. package/src/lib/server/process-restock.js.map +1 -1
  112. package/src/lib/server/product-record-index.d.ts +19 -0
  113. package/src/lib/server/product-record-index.js +83 -0
  114. package/src/lib/server/product-record-index.js.map +1 -0
  115. package/src/lib/server/reconcile-stock.js +1 -1
  116. package/src/lib/server/reconcile-stock.js.map +1 -1
  117. package/src/lib/server/reserve-stock.js +6 -1
  118. package/src/lib/server/reserve-stock.js.map +1 -1
  119. package/src/lib/server/reserve.js +2 -2
  120. package/src/lib/server/reserve.js.map +1 -1
  121. package/src/lib/server/revenue-source.d.ts +130 -0
  122. package/src/lib/server/revenue-source.js +286 -0
  123. package/src/lib/server/revenue-source.js.map +1 -0
  124. package/src/lib/server/supplier-outbox.js +16 -1
  125. package/src/lib/server/supplier-outbox.js.map +1 -1
  126. package/src/lib/server/supplier-update.js +19 -9
  127. package/src/lib/server/supplier-update.js.map +1 -1
  128. package/src/lib/server/tax-profile.d.ts +8 -5
  129. package/src/lib/server/tax-profile.js +22 -5
  130. package/src/lib/server/tax-profile.js.map +1 -1
  131. package/src/lib/server/tax-return-source.d.ts +125 -0
  132. package/src/lib/server/tax-return-source.js +524 -0
  133. package/src/lib/server/tax-return-source.js.map +1 -0
  134. package/src/lib/server/tax-summary.d.ts +92 -0
  135. package/src/lib/server/tax-summary.js +258 -0
  136. package/src/lib/server/tax-summary.js.map +1 -0
  137. package/src/lib/server.d.ts +1 -0
  138. package/src/lib/server.js +23 -0
  139. package/src/lib/server.js.map +1 -1
  140. package/src/lib/starter-templates.d.ts +19 -0
  141. package/src/lib/starter-templates.js +157 -0
  142. package/src/lib/starter-templates.js.map +1 -0
  143. package/src/lib/tenant-emails.d.ts +33 -0
  144. package/src/lib/tenant-emails.js +551 -0
  145. package/src/lib/tenant-emails.js.map +1 -0
  146. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  147. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  148. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
@@ -0,0 +1,92 @@
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
+ /**
18
+ * The sales tax a merchant's OWN storefront collected (AGL-2440).
19
+ *
20
+ * `GET /api/commerce/tax-summary?hostId=…&from=…&to=…`, served by the
21
+ * console's plugin dispatcher.
22
+ *
23
+ * `storefrontTaxCollected` has recorded every taxed storefront sale since
24
+ * AGL-1904, and every other reader of it is the operator's: the staff
25
+ * tax-return source sums it for the operator's own return, and the DSAR
26
+ * export includes it. The merchant — whose sales these are — had no report
27
+ * at all. Their only tax visibility was one order's `totals.taxCents` at a
28
+ * time. This is that
29
+ * absence closed, and closing it is the whole of what this handler does.
30
+ *
31
+ * ## WHAT THIS HANDLER DOES NOT DO, and must never be extended to do
32
+ *
33
+ * It emits NO per-merchant, per-jurisdiction verdict on who must remit — and
34
+ * that is still true now that the facilitator question itself is answered.
35
+ * Aglyn IS a marketplace facilitator for storefront sales, and Terms §10.7
36
+ * publishes that (AGL-1956). But §10.7 is
37
+ * narrow by design: Aglyn calculates, collects and remits **where applicable
38
+ * law gives it an obligation for that transaction**, and asserts nothing
39
+ * anywhere else. A field answering "is Aglyn the facilitator for you" would
40
+ * flatten a per-transaction, per-jurisdiction test into one boolean about a
41
+ * merchant, which the Terms deliberately decline to state.
42
+ *
43
+ * So the response still carries the three buckets SEPARATELY and never a merged
44
+ * total. There is deliberately no field called `yoursToRemit`. Getting that
45
+ * wrong in either direction is worse than the absence it would fill:
46
+ * understate it and a merchant under-remits on our say-so;
47
+ * overstate it and they pay tax twice on money Aglyn already holds. A single
48
+ * "tax collected" figure is the same error wearing a neutral name — it would
49
+ * sum tax Aglyn holds and remits with tax the merchant owes, which is exactly
50
+ * the conflation `storefront-tax.ts` warns about in bold: *the two store modes
51
+ * are DIFFERENT FACTS and must never be summed.*
52
+ *
53
+ * `storefrontTaxSummary` is reused rather than reimplemented, so the merchant
54
+ * and the staff return read the same rows through the same classifier. It
55
+ * buckets off the STORED `taxMode`/`taxLiability` and drops an unrecognised
56
+ * one into `attention.rowsUnclassified` rather than defaulting it into a
57
+ * bucket — the property that makes a separated answer trustworthy.
58
+ *
59
+ * ## Why an Admin-SDK handler and not a client read
60
+ *
61
+ * `cloud/firebase-firestore.rules` denies `storefrontTaxCollected` to every
62
+ * client (`allow read, write: if false`) and that stays exactly as it is: the
63
+ * collection spans every merchant, and a row carries a shopper's address
64
+ * beside the amounts. Rules do not apply to the Admin SDK, so this handler needs
65
+ * NO rules change — the deny is what makes a server handler the only correct
66
+ * shape, not an obstacle to it. The `hostId` filter below is therefore the
67
+ * whole of the tenant boundary and is not optional.
68
+ *
69
+ * ## Reach
70
+ *
71
+ * Host membership, with an org-membership fallback — the same bar as
72
+ * `/api/hosts/usage`. The workspace admins who own billing are frequently not
73
+ * members of the site itself, and they are the people who file. A 404 rather
74
+ * than a 403 on refusal, so the handler never confirms a site exists to
75
+ * someone who cannot see it.
76
+ *
77
+ * ## The query needs a composite index
78
+ *
79
+ * `hostId ==` plus a `paidAt` range is a composite query;
80
+ * `cloud/firebase-firestore.indexes.json` gains
81
+ * `storefrontTaxCollected (hostId ASC, paidAt ASC)` for it. Indexes deploy
82
+ * MANUALLY, outside the git→Vercel pipeline, so a merged commit is not
83
+ * evidence the index shipped — the card renders an error until it has.
84
+ *
85
+ * The alternative was a single-field `hostId ==` read filtered in memory,
86
+ * which needs no index. It was rejected: without an index there is no
87
+ * `orderBy`, so hitting the row cap would truncate an ARBITRARY subset, and a
88
+ * tax total computed from an arbitrary subset is not a partial answer — it is
89
+ * a wrong one that looks right. With the index, truncation is the oldest rows
90
+ * of a known period and is reported as `truncated`.
91
+ */
92
+ export declare function taxSummaryHandler(request: Request): Promise<Response>;
@@ -0,0 +1,258 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { pluginRequestFromWeb } from "@aglyn/aglyn/server";
18
+ import { emailUnverifiedResponse, firebaseAdmin, getOrgForHost, isImpersonationSession, lockdownRefusal, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
19
+ import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
20
+ import { storefrontTaxSummary } from "../model/storefront-tax-summary.js";
21
+ /** Rows read per request. Past this the answer is reported as PARTIAL. */ const ROW_CAP = 2000;
22
+ /**
23
+ * The sales tax a merchant's OWN storefront collected (AGL-2440).
24
+ *
25
+ * `GET /api/commerce/tax-summary?hostId=…&from=…&to=…`, served by the
26
+ * console's plugin dispatcher.
27
+ *
28
+ * `storefrontTaxCollected` has recorded every taxed storefront sale since
29
+ * AGL-1904, and every other reader of it is the operator's: the staff
30
+ * tax-return source sums it for the operator's own return, and the DSAR
31
+ * export includes it. The merchant — whose sales these are — had no report
32
+ * at all. Their only tax visibility was one order's `totals.taxCents` at a
33
+ * time. This is that
34
+ * absence closed, and closing it is the whole of what this handler does.
35
+ *
36
+ * ## WHAT THIS HANDLER DOES NOT DO, and must never be extended to do
37
+ *
38
+ * It emits NO per-merchant, per-jurisdiction verdict on who must remit — and
39
+ * that is still true now that the facilitator question itself is answered.
40
+ * Aglyn IS a marketplace facilitator for storefront sales, and Terms §10.7
41
+ * publishes that (AGL-1956). But §10.7 is
42
+ * narrow by design: Aglyn calculates, collects and remits **where applicable
43
+ * law gives it an obligation for that transaction**, and asserts nothing
44
+ * anywhere else. A field answering "is Aglyn the facilitator for you" would
45
+ * flatten a per-transaction, per-jurisdiction test into one boolean about a
46
+ * merchant, which the Terms deliberately decline to state.
47
+ *
48
+ * So the response still carries the three buckets SEPARATELY and never a merged
49
+ * total. There is deliberately no field called `yoursToRemit`. Getting that
50
+ * wrong in either direction is worse than the absence it would fill:
51
+ * understate it and a merchant under-remits on our say-so;
52
+ * overstate it and they pay tax twice on money Aglyn already holds. A single
53
+ * "tax collected" figure is the same error wearing a neutral name — it would
54
+ * sum tax Aglyn holds and remits with tax the merchant owes, which is exactly
55
+ * the conflation `storefront-tax.ts` warns about in bold: *the two store modes
56
+ * are DIFFERENT FACTS and must never be summed.*
57
+ *
58
+ * `storefrontTaxSummary` is reused rather than reimplemented, so the merchant
59
+ * and the staff return read the same rows through the same classifier. It
60
+ * buckets off the STORED `taxMode`/`taxLiability` and drops an unrecognised
61
+ * one into `attention.rowsUnclassified` rather than defaulting it into a
62
+ * bucket — the property that makes a separated answer trustworthy.
63
+ *
64
+ * ## Why an Admin-SDK handler and not a client read
65
+ *
66
+ * `cloud/firebase-firestore.rules` denies `storefrontTaxCollected` to every
67
+ * client (`allow read, write: if false`) and that stays exactly as it is: the
68
+ * collection spans every merchant, and a row carries a shopper's address
69
+ * beside the amounts. Rules do not apply to the Admin SDK, so this handler needs
70
+ * NO rules change — the deny is what makes a server handler the only correct
71
+ * shape, not an obstacle to it. The `hostId` filter below is therefore the
72
+ * whole of the tenant boundary and is not optional.
73
+ *
74
+ * ## Reach
75
+ *
76
+ * Host membership, with an org-membership fallback — the same bar as
77
+ * `/api/hosts/usage`. The workspace admins who own billing are frequently not
78
+ * members of the site itself, and they are the people who file. A 404 rather
79
+ * than a 403 on refusal, so the handler never confirms a site exists to
80
+ * someone who cannot see it.
81
+ *
82
+ * ## The query needs a composite index
83
+ *
84
+ * `hostId ==` plus a `paidAt` range is a composite query;
85
+ * `cloud/firebase-firestore.indexes.json` gains
86
+ * `storefrontTaxCollected (hostId ASC, paidAt ASC)` for it. Indexes deploy
87
+ * MANUALLY, outside the git→Vercel pipeline, so a merged commit is not
88
+ * evidence the index shipped — the card renders an error until it has.
89
+ *
90
+ * The alternative was a single-field `hostId ==` read filtered in memory,
91
+ * which needs no index. It was rejected: without an index there is no
92
+ * `orderBy`, so hitting the row cap would truncate an ARBITRARY subset, and a
93
+ * tax total computed from an arbitrary subset is not a partial answer — it is
94
+ * a wrong one that looks right. With the index, truncation is the oldest rows
95
+ * of a known period and is reported as `truncated`.
96
+ */ export async function taxSummaryHandler(request) {
97
+ var _headers_authorization, _ref;
98
+ const { method, headers: rawHeaders, query } = await pluginRequestFromWeb(request);
99
+ const headers = rawHeaders;
100
+ if (method !== 'GET') {
101
+ return Response.json({
102
+ error: 'Method not allowed'
103
+ }, {
104
+ status: 405
105
+ });
106
+ }
107
+ const authorization = (_headers_authorization = headers.authorization) != null ? _headers_authorization : '';
108
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
109
+ if (!idToken) {
110
+ return Response.json({
111
+ error: 'Unauthenticated'
112
+ }, {
113
+ status: 401
114
+ });
115
+ }
116
+ // Query params, not a body: a GET that carries one is dropped before it
117
+ // leaves the browser, with no request and no status to debug.
118
+ const hostId = String((_ref = query == null ? void 0 : query['hostId']) != null ? _ref : '');
119
+ if (!hostId) {
120
+ return Response.json({
121
+ error: 'Missing hostId'
122
+ }, {
123
+ status: 400
124
+ });
125
+ }
126
+ const range = resolveRange(query == null ? void 0 : query['from'], query == null ? void 0 : query['to']);
127
+ if (!range) {
128
+ return Response.json({
129
+ error: 'Invalid period'
130
+ }, {
131
+ status: 400
132
+ });
133
+ }
134
+ try {
135
+ var _hostSnapshot_get;
136
+ var _this;
137
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
138
+ if (!decoded.email_verified && !isImpersonationSession(decoded)) {
139
+ return emailUnverifiedResponse();
140
+ }
141
+ const firestore = firebaseAdmin.app().firestore();
142
+ const hostRef = firestore.collection('hosts').doc(hostId);
143
+ const hostSnapshot = await hostRef.get();
144
+ if (!hostSnapshot.exists) {
145
+ return Response.json({
146
+ error: 'Not found'
147
+ }, {
148
+ status: 404
149
+ });
150
+ }
151
+ // Whoever may read the site may read what its storefront collected. Host
152
+ // membership covers editors and collaborators; the org fallback covers the
153
+ // workspace admins who own billing but were never added to the site — the
154
+ // people this report is actually for.
155
+ const isHostMember = Boolean(((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid]);
156
+ let allowed = isHostMember;
157
+ if (!allowed) {
158
+ const orgId = await resolveOrgIdForHost(hostId);
159
+ if (orgId) {
160
+ const orgMember = await firestore.collection('orgs').doc(orgId).collection('members').doc(decoded.uid).get();
161
+ allowed = orgMember.exists;
162
+ }
163
+ }
164
+ if (!allowed) {
165
+ return Response.json({
166
+ error: 'Not found'
167
+ }, {
168
+ status: 404
169
+ });
170
+ }
171
+ const locked = await lockdownRefusal({
172
+ request,
173
+ staff: decoded['staff'] === true,
174
+ uid: decoded.uid,
175
+ org: (_this = await getOrgForHost(hostId)) == null ? void 0 : _this.org,
176
+ host: hostSnapshot.data()
177
+ });
178
+ if (locked) return locked;
179
+ // THE TENANT BOUNDARY. `hostId ==` is the only thing standing between one
180
+ // merchant and every other merchant's sales, because rules do not run
181
+ // here. It is applied in the query rather than after the read so a
182
+ // mistake downstream cannot widen it.
183
+ const [inPeriod, undated] = await Promise.all([
184
+ firestore.collection('storefrontTaxCollected').where('hostId', '==', hostId).where('paidAt', '>=', range.start).where('paidAt', '<', range.end).limit(ROW_CAP + 1).get(),
185
+ // A null `paidAt` is INVISIBLE to a range query, so a row that failed to
186
+ // date itself would silently vanish from the total rather than be
187
+ // reported missing. Counted out loud, exactly as the staff return does:
188
+ // an aggregate over every such row, not the size of a capped read.
189
+ firestore.collection('storefrontTaxCollected').where('hostId', '==', hostId).where('paidAt', '==', null).count().get()
190
+ ]);
191
+ const truncated = inPeriod.size > ROW_CAP;
192
+ const rows = inPeriod.docs.slice(0, ROW_CAP).map((doc)=>_extends({
193
+ id: doc.id
194
+ }, doc.data()));
195
+ return Response.json({
196
+ hostId,
197
+ period: {
198
+ from: range.start.toISOString(),
199
+ to: range.end.toISOString()
200
+ },
201
+ summary: storefrontTaxSummary(rows, range),
202
+ truncated,
203
+ undatedRows: undated.data().count,
204
+ /**
205
+ * NOT A REMITTANCE DETERMINATION. Every one of these is a fact about
206
+ * the DATA — what it does not yet reflect, and what it could not read
207
+ * — so a merchant can tell how complete the number is before they use
208
+ * it. None of them says whose tax it is.
209
+ */ caveats: {
210
+ // The writer keeps a refunded sale's full tax on its record. The
211
+ // direction is deliberate — over-stating what is held for a state is
212
+ // correctable at filing, under-stating it is a shortfall an auditor
213
+ // finds — but a merchant reading their own figure has to be told.
214
+ refundsNotReflected: true
215
+ }
216
+ }, {
217
+ status: 200
218
+ });
219
+ } catch (error) {
220
+ // A refused credential is a 401, not a fault of ours (AGL-1993). Null
221
+ // for anything else, so a real failure keeps the answer below.
222
+ const unauthenticated = invalidIdTokenResponse(error);
223
+ if (unauthenticated) return unauthenticated;
224
+ console.error(error);
225
+ return Response.json({
226
+ error: 'Tax summary failed'
227
+ }, {
228
+ status: 500
229
+ });
230
+ }
231
+ }
232
+ /**
233
+ * The reporting window. Defaults to the current calendar month in UTC, which
234
+ * is what a monthly filing period means when no timezone has been declared.
235
+ *
236
+ * A malformed date answers `null` and the handler 400s rather than silently
237
+ * substituting the default: a merchant who asked for Q1 and received the
238
+ * current month would file the wrong number and have no way to notice.
239
+ */ function resolveRange(from, to) {
240
+ const now = new Date();
241
+ const parse = (value)=>{
242
+ if (value === undefined || value === null || value === '') return undefined;
243
+ const parsed = new Date(String(value));
244
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
245
+ };
246
+ const parsedFrom = parse(from);
247
+ const parsedTo = parse(to);
248
+ if (parsedFrom === null || parsedTo === null) return null;
249
+ const start = parsedFrom != null ? parsedFrom : new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
250
+ const end = parsedTo != null ? parsedTo : new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
251
+ if (end <= start) return null;
252
+ return {
253
+ start,
254
+ end
255
+ };
256
+ }
257
+
258
+ //# sourceMappingURL=tax-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/tax-summary.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 { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n getOrgForHost,\n isImpersonationSession,\n lockdownRefusal,\n resolveOrgIdForHost,\n} from '@aglyn/tenant-data-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n storefrontTaxSummary,\n type StorefrontTaxReturnRowInput,\n} from '../model/storefront-tax-summary'\n\n/** Rows read per request. Past this the answer is reported as PARTIAL. */\nconst ROW_CAP = 2000\n\n/**\n * The sales tax a merchant's OWN storefront collected (AGL-2440).\n *\n * `GET /api/commerce/tax-summary?hostId=…&from=…&to=…`, served by the\n * console's plugin dispatcher.\n *\n * `storefrontTaxCollected` has recorded every taxed storefront sale since\n * AGL-1904, and every other reader of it is the operator's: the staff\n * tax-return source sums it for the operator's own return, and the DSAR\n * export includes it. The merchant — whose sales these are — had no report\n * at all. Their only tax visibility was one order's `totals.taxCents` at a\n * time. This is that\n * absence closed, and closing it is the whole of what this handler does.\n *\n * ## WHAT THIS HANDLER DOES NOT DO, and must never be extended to do\n *\n * It emits NO per-merchant, per-jurisdiction verdict on who must remit — and\n * that is still true now that the facilitator question itself is answered.\n * Aglyn IS a marketplace facilitator for storefront sales, and Terms §10.7\n * publishes that (AGL-1956). But §10.7 is\n * narrow by design: Aglyn calculates, collects and remits **where applicable\n * law gives it an obligation for that transaction**, and asserts nothing\n * anywhere else. A field answering \"is Aglyn the facilitator for you\" would\n * flatten a per-transaction, per-jurisdiction test into one boolean about a\n * merchant, which the Terms deliberately decline to state.\n *\n * So the response still carries the three buckets SEPARATELY and never a merged\n * total. There is deliberately no field called `yoursToRemit`. Getting that\n * wrong in either direction is worse than the absence it would fill:\n * understate it and a merchant under-remits on our say-so;\n * overstate it and they pay tax twice on money Aglyn already holds. A single\n * \"tax collected\" figure is the same error wearing a neutral name — it would\n * sum tax Aglyn holds and remits with tax the merchant owes, which is exactly\n * the conflation `storefront-tax.ts` warns about in bold: *the two store modes\n * are DIFFERENT FACTS and must never be summed.*\n *\n * `storefrontTaxSummary` is reused rather than reimplemented, so the merchant\n * and the staff return read the same rows through the same classifier. It\n * buckets off the STORED `taxMode`/`taxLiability` and drops an unrecognised\n * one into `attention.rowsUnclassified` rather than defaulting it into a\n * bucket — the property that makes a separated answer trustworthy.\n *\n * ## Why an Admin-SDK handler and not a client read\n *\n * `cloud/firebase-firestore.rules` denies `storefrontTaxCollected` to every\n * client (`allow read, write: if false`) and that stays exactly as it is: the\n * collection spans every merchant, and a row carries a shopper's address\n * beside the amounts. Rules do not apply to the Admin SDK, so this handler needs\n * NO rules change — the deny is what makes a server handler the only correct\n * shape, not an obstacle to it. The `hostId` filter below is therefore the\n * whole of the tenant boundary and is not optional.\n *\n * ## Reach\n *\n * Host membership, with an org-membership fallback — the same bar as\n * `/api/hosts/usage`. The workspace admins who own billing are frequently not\n * members of the site itself, and they are the people who file. A 404 rather\n * than a 403 on refusal, so the handler never confirms a site exists to\n * someone who cannot see it.\n *\n * ## The query needs a composite index\n *\n * `hostId ==` plus a `paidAt` range is a composite query;\n * `cloud/firebase-firestore.indexes.json` gains\n * `storefrontTaxCollected (hostId ASC, paidAt ASC)` for it. Indexes deploy\n * MANUALLY, outside the git→Vercel pipeline, so a merged commit is not\n * evidence the index shipped — the card renders an error until it has.\n *\n * The alternative was a single-field `hostId ==` read filtered in memory,\n * which needs no index. It was rejected: without an index there is no\n * `orderBy`, so hitting the row cap would truncate an ARBITRARY subset, and a\n * tax total computed from an arbitrary subset is not a partial answer — it is\n * a wrong one that looks right. With the index, truncation is the oldest rows\n * of a known period and is reported as `truncated`.\n */\nexport async function taxSummaryHandler(request: Request): Promise<Response> {\n const { method, headers: rawHeaders, query } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n\n // Query params, not a body: a GET that carries one is dropped before it\n // leaves the browser, with no request and no status to debug.\n const hostId = String(query?.['hostId'] ?? '')\n if (!hostId) {\n return Response.json({ error: 'Missing hostId' }, { status: 400 })\n }\n const range = resolveRange(query?.['from'], query?.['to'])\n if (!range) {\n return Response.json({ error: 'Invalid period' }, { status: 400 })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n\n // Whoever may read the site may read what its storefront collected. Host\n // membership covers editors and collaborators; the org fallback covers the\n // workspace admins who own billing but were never added to the site — the\n // people this report is actually for.\n const isHostMember = Boolean(\n (hostSnapshot.get('memberRoles') ?? {})[decoded.uid],\n )\n let allowed = isHostMember\n if (!allowed) {\n const orgId = await resolveOrgIdForHost(hostId)\n if (orgId) {\n const orgMember = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('members')\n .doc(decoded.uid)\n .get()\n allowed = orgMember.exists\n }\n }\n if (!allowed) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n\n const locked = await lockdownRefusal({\n request,\n staff: decoded['staff'] === true,\n uid: decoded.uid,\n org: (await getOrgForHost(hostId))?.org,\n host: hostSnapshot.data(),\n })\n if (locked) return locked\n\n // THE TENANT BOUNDARY. `hostId ==` is the only thing standing between one\n // merchant and every other merchant's sales, because rules do not run\n // here. It is applied in the query rather than after the read so a\n // mistake downstream cannot widen it.\n const [inPeriod, undated] = await Promise.all([\n firestore\n .collection('storefrontTaxCollected')\n .where('hostId', '==', hostId)\n .where('paidAt', '>=', range.start)\n .where('paidAt', '<', range.end)\n .limit(ROW_CAP + 1)\n .get(),\n // A null `paidAt` is INVISIBLE to a range query, so a row that failed to\n // date itself would silently vanish from the total rather than be\n // reported missing. Counted out loud, exactly as the staff return does:\n // an aggregate over every such row, not the size of a capped read.\n firestore\n .collection('storefrontTaxCollected')\n .where('hostId', '==', hostId)\n .where('paidAt', '==', null)\n .count()\n .get(),\n ])\n\n const truncated = inPeriod.size > ROW_CAP\n const rows: StorefrontTaxReturnRowInput[] = inPeriod.docs\n .slice(0, ROW_CAP)\n .map((doc) => ({ id: doc.id, ...(doc.data() as object) }) as StorefrontTaxReturnRowInput)\n\n return Response.json(\n {\n hostId,\n period: { from: range.start.toISOString(), to: range.end.toISOString() },\n summary: storefrontTaxSummary(rows, range),\n truncated,\n undatedRows: undated.data().count,\n /**\n * NOT A REMITTANCE DETERMINATION. Every one of these is a fact about\n * the DATA — what it does not yet reflect, and what it could not read\n * — so a merchant can tell how complete the number is before they use\n * it. None of them says whose tax it is.\n */\n caveats: {\n // The writer keeps a refunded sale's full tax on its record. The\n // direction is deliberate — over-stating what is held for a state is\n // correctable at filing, under-stating it is a shortfall an auditor\n // finds — but a merchant reading their own figure has to be told.\n refundsNotReflected: true,\n },\n },\n { status: 200 },\n )\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error(error)\n return Response.json({ error: 'Tax summary failed' }, { status: 500 })\n }\n}\n\n/**\n * The reporting window. Defaults to the current calendar month in UTC, which\n * is what a monthly filing period means when no timezone has been declared.\n *\n * A malformed date answers `null` and the handler 400s rather than silently\n * substituting the default: a merchant who asked for Q1 and received the\n * current month would file the wrong number and have no way to notice.\n */\nfunction resolveRange(\n from: unknown,\n to: unknown,\n): { start: Date; end: Date } | null {\n const now = new Date()\n const parse = (value: unknown): Date | null | undefined => {\n if (value === undefined || value === null || value === '') return undefined\n const parsed = new Date(String(value))\n return Number.isNaN(parsed.getTime()) ? null : parsed\n }\n const parsedFrom = parse(from)\n const parsedTo = parse(to)\n if (parsedFrom === null || parsedTo === null) return null\n const start =\n parsedFrom ??\n new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))\n const end =\n parsedTo ??\n new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))\n if (end <= start) return null\n return { start, end }\n}\n\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","getOrgForHost","isImpersonationSession","lockdownRefusal","resolveOrgIdForHost","invalidIdTokenResponse","storefrontTaxSummary","ROW_CAP","taxSummaryHandler","request","headers","method","rawHeaders","query","Response","json","error","status","authorization","idToken","startsWith","slice","length","undefined","hostId","String","range","resolveRange","hostSnapshot","decoded","app","auth","verifyIdToken","email_verified","firestore","hostRef","collection","doc","get","exists","isHostMember","Boolean","uid","allowed","orgId","orgMember","locked","staff","org","host","data","inPeriod","undated","Promise","all","where","start","end","limit","count","truncated","size","rows","docs","map","id","period","from","toISOString","to","summary","undatedRows","caveats","refundsNotReflected","unauthenticated","console","now","Date","parse","value","parsed","Number","isNaN","getTime","parsedFrom","parsedTo","UTC","getUTCFullYear","getUTCMonth"],"mappings":"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"}
@@ -30,3 +30,4 @@ export declare function registerCommerceApi(): void;
30
30
  export declare function registerCommerceConsoleApi(): void;
31
31
  export { mintDownloadToken } from './server/download';
32
32
  export * from './server/membership';
33
+ export type { commercePlanEntitlements } from './plan-entitlements';
package/src/lib/server.js CHANGED
@@ -28,6 +28,11 @@ import { registerCommerceMediaPublishGuard } from "./server/media-publish-guard.
28
28
  import { registerCommerceRecurringCharges } from "./server/recurring-charges.js";
29
29
  import { registerProductCardReader } from "./server/product-card.js";
30
30
  import { registerCommerceTaxProfile } from "./server/tax-profile.js";
31
+ import { registerTaxReturnSource } from "@aglyn/aglyn/plugin-manager/plugin-tax-return-sources";
32
+ import { commerceTaxReturnSource } from "./server/tax-return-source.js";
33
+ import { taxSummaryHandler } from "./server/tax-summary.js";
34
+ import { registerRevenueSource } from "@aglyn/aglyn/plugin-manager/plugin-revenue-sources";
35
+ import { commerceRevenueSource } from "./server/revenue-source.js";
31
36
  import { commerceBillingWebhookHandler } from "./server/billing-webhook.js";
32
37
  import { registerOrderFigureReaders } from "./server/order-figures.js";
33
38
  import { COMMERCE_PERMISSIONS } from "./model/plugin-permissions.js";
@@ -273,6 +278,19 @@ registerPluginJob({
273
278
  registerCommerceRecurringCharges();
274
279
  // …and the tax rule, for the webhook that confirms what they charged.
275
280
  registerCommerceTaxProfile();
281
+ // …and its sales on the operator's own sales tax return (AGL-3080): the
282
+ // tax a storefront checkout collected under the platform's registrations.
283
+ // The staff return awaits this surface before it asks, and refuses to be
284
+ // filed without this source — commerce declares `taxReturnSource`.
285
+ registerTaxReturnSource(commerceTaxReturnSource, {
286
+ pluginId: BUNDLE_ID
287
+ });
288
+ // …and the take its orders earn the operator, on the staff revenue report
289
+ // (AGL-3080) — commerce declares `revenueSource`, so the report names this
290
+ // source as unread rather than counting it as zero if it is missing.
291
+ registerRevenueSource(commerceRevenueSource, {
292
+ pluginId: BUNDLE_ID
293
+ });
276
294
  // Stripe webhook sections (AGL-418): orders/carts/drafts/reservations/
277
295
  // subscriptions ride the platform webhook via the hook registry.
278
296
  registerBillingWebhookHandler(commerceBillingWebhookHandler);
@@ -308,6 +326,11 @@ registerPluginJob({
308
326
  // catch-all in the Firestore rules would otherwise let a client write
309
327
  // its own `balanceCents`, which checkout applies as amount-off.
310
328
  registerPluginApiRoute('commerce/gift-cards', giftCardsHandler);
329
+ // The merchant's own storefront sales tax, by who owes it (AGL-2440): the
330
+ // same rows and classifier the operator's return reads, fenced to one site.
331
+ registerPluginApiRoute('commerce/tax-summary', {
332
+ web: taxSummaryHandler
333
+ });
311
334
  // A smart collection's rules changed or it was deleted: re-stamp which
312
335
  // products it holds (AGL-3321), the membership the storefront queries.
313
336
  registerPluginApiRoute('commerce/collection-membership', collectionMembershipHandler);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/server.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\n\n/**\n * Server half of the commerce plugin (AGL-396): the site-facing storefront\n * API handlers, registered with the plugin API registry and served by the\n * tenant dispatcher at their unchanged `/api/commerce/*` URLs. This module\n * pulls in firebase-admin + Stripe, so it is NOT re-exported from the client\n * barrel — apps import `@aglyn/plugins-commerce/server` only from their\n * (server-only) API dispatcher registration.\n */\n\nimport {\n registerBillingWebhookHandler,\n registerOrderFulfilmentService,\n registerPluginApiRoute,\n registerPluginJob,\n registerSitePageEnricher,\n registerSitePageResolver,\n registerPluginPermissions,\n registerPluginConfigSchema,\n} from '@aglyn/aglyn/server'\nimport { isEmailConfigured } from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCommerceMediaPublishGuard } from './server/media-publish-guard'\nimport { registerCommerceRecurringCharges } from './server/recurring-charges'\nimport { registerProductCardReader } from './server/product-card'\nimport { registerCommerceTaxProfile } from './server/tax-profile'\nimport { commerceBillingWebhookHandler } from './server/billing-webhook'\nimport { registerOrderFigureReaders } from './server/order-figures'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\nimport { commerceSitePageEnricher } from './server/site-page-enricher'\nimport { commerceSitePageResolver } from './server/site-page-resolver'\nimport { cartCheckoutHandler } from './server/cart-checkout'\nimport { cartHandler } from './server/cart'\nimport { catalogHandler } from './server/catalog'\nimport { checkoutHandler } from './server/checkout'\nimport { downloadHandler } from './server/download'\nimport { feedHandler } from './server/feed'\nimport { newsletterHandler } from './server/newsletter'\nimport { notifyRestockHandler } from './server/notify-restock'\nimport { productHandler } from './server/product'\nimport { relatedHandler } from './server/related'\nimport { reservationAvailabilityHandler } from './server/reservation-availability'\nimport { gateHandler } from './server/gate'\nimport { memberFeedHandler } from './server/member-feed'\nimport { membershipAccountHandler } from './server/membership-account'\nimport { membershipAdminPasswordHandler } from './server/membership-admin-password'\nimport { membershipAdminRemoveHandler } from './server/membership-admin-remove'\nimport { membershipContentHandler } from './server/membership-content'\nimport { membershipLoginHandler } from './server/membership-login'\nimport { membershipLogoutHandler } from './server/membership-logout'\nimport { membershipRecoverHandler } from './server/membership-recover'\nimport { membershipRegisterHandler } from './server/membership-register'\nimport { membershipResetHandler } from './server/membership-reset'\nimport { membershipWishlistHandler } from './server/membership-wishlist'\nimport { reserveHandler } from './server/reserve'\nimport { streamHandler } from './server/stream'\nimport { subscriptionPortalHandler } from './server/subscription-portal'\nimport { reviewsHandler } from './server/reviews'\nimport { connectHandler } from './server/connect'\nimport { cancelOrderHandler } from './server/cancel-order'\nimport { collectionMembershipHandler } from './server/collection-membership'\nimport { draftOrderHandler } from './server/draft-order'\nimport {\n fulfillOrderHandler,\n recordOrderShipment,\n} from './server/fulfill-order'\nimport { giftCardsHandler } from './server/gift-cards'\nimport { memberPostHandler } from './server/member-post'\nimport { orderAnalyticsHandler } from './server/order-analytics'\nimport { posOrderHandler } from './server/pos-order'\nimport {\n processAbandonedHandler,\n scanAbandonedCheckouts,\n} from './server/process-abandoned'\nimport { processRestockHandler, scanRestockAlerts } from './server/process-restock'\nimport { scanStockDecrements } from './server/reconcile-stock'\nimport { refundHandler } from './server/refund'\nimport { scanSupplierDeliveries } from './server/supplier-outbox'\nimport { supplierUpdateHandler } from './server/supplier-update'\n\n/**\n * The two commerce beats (AGL-2227).\n *\n * `commerce/process-abandoned` and `commerce/process-restock` have existed\n * since AGL-323/326 as `x-cron-secret` HTTP doors, and `registerCommerceConsoleApi`\n * below has called them \"the scheduler-driven jobs\" in a comment that whole\n * time. **Nothing scheduled them.** Not `scheduled-crons.yml` (its table names\n * 11 paths; neither of these), not `vercel.json` (no `crons` key at all), not\n * `registerPluginJob` — the only two registrations in the repo were scheduled\n * publishing and the bookings hold-expiry.\n *\n * The cost was a Pro-tier entitlement (`abandonedCart`) that had never sent an\n * email, and a storefront \"notify me when it's back\" form writing into a queue\n * with no drain. AGL-1793 had already added the collection-group indexes these\n * two scans need, which is the clearest evidence they were always meant to run.\n *\n * Module scope, like the bookings job beside it: the runner route reaches jobs\n * through `ensureAll(['tenantApi'])`, and a registration inside a `register*`\n * function would depend on which entry point happened to be loaded.\n *\n * 15 minutes, not 1: `process-abandoned` will not remind a checkout younger\n * than an hour and `process-restock` only mails alerts whose product already\n * has stock, so a tighter beat buys nothing and costs two collection-group\n * scans a minute. Both are bounded (200 docs) and idempotent — each pass\n * stamps what it sent — so an overlapping or repeated beat cannot double-send.\n */\nconst RECOVERY_JOB_INTERVAL_MINUTES = 15\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'abandoned-checkout-recovery',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description:\n 'Email one recovery reminder per stalled checkout (abandonedCart plans).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n // Quietly, not as an error: email is optional per deployment, and a beat\n // that logs every minute on a self-host without Resend buries everything\n // else in the log.\n if (!isEmailConfigured()) return\n const { sent } = await scanAbandonedCheckouts(gate)\n if (sent) console.info(`commerce: sent ${sent} abandoned-cart reminders`)\n },\n})\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'back-in-stock-alerts',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description: 'Email shoppers whose requested product is in stock again.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n if (!isEmailConfigured()) return\n const { sent } = await scanRestockAlerts(gate)\n if (sent) console.info(`commerce: sent ${sent} back-in-stock alerts`)\n },\n})\n\n/**\n * The missing-decrement detector (AGL-2358).\n *\n * HOURLY, not on the 15-minute recovery beat: it is a detector for a rare\n * process death, not a queue drain, and nothing it finds gets less true for\n * waiting an hour. The cost of the beat is what sets the interval — one\n * collection-group read of the platform's most recent orders, plus two\n * queries per host that has any — and an hour keeps that off the same tick as\n * the two scans above.\n *\n * NOT gated on `isEmailConfigured()`, unlike its two neighbours. Those send\n * mail; this writes a console notification and a log line, both of which work\n * on a self-host with no mail provider at all — and a stock count that is\n * silently wrong is exactly what a self-hoster least wants suppressed by a\n * setting about email.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'stock-decrement-reconciliation',\n intervalMinutes: 60,\n description:\n 'Report paid orders whose stock decrement never landed (AGL-2358).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanStockDecrements(gate)\n if (scan.missingLines || scan.truncatedHosts) {\n console.warn(\n `commerce: ${scan.missingLines} order lines across ${scan.hosts} ` +\n `sites have no sale ledger row (${scan.reportedOrders} newly ` +\n `reported, ${scan.truncatedHosts} sites' ledger window truncated)`,\n )\n }\n },\n})\n\n/**\n * The dropship supplier outbox drain (AGL-2473).\n *\n * EVERY MINUTE, unlike the three above, and the interval is the point rather\n * than an oversight. This is the only one of the four that a BUYER is waiting\n * on: the row it drains is a paid order that has not been routed to whoever\n * ships it, and every minute it sits is a minute the parcel is not moving. The\n * first backoff step is also 60s, so a supplier that blipped is retried on the\n * next tick.\n *\n * The cost that set fifteen minutes for the recovery pair does not apply. Those\n * are collection-GROUP scans across every site on the platform; this is one\n * ordinary equality query against a top-level collection that is EMPTY in the\n * ordinary case — an empty result bills a single read, so the whole beat is\n * ~1,440 reads a day whether or not anyone is dropshipping.\n *\n * Not gated on `isEmailConfigured()`. The supplier notification is an HTTP POST\n * to the merchant's own supplier, and a self-host with no mail provider still\n * has dropship orders to route.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'supplier-webhook-delivery',\n intervalMinutes: 1,\n description:\n 'Deliver queued dropship supplier notifications, with backoff and a ' +\n 'dead letter that tells the merchant (AGL-2473).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanSupplierDeliveries(gate)\n if (scan.deadLettered) {\n console.warn(\n `commerce: ${scan.deadLettered} dropship orders could not be routed ` +\n `to their supplier and have been reported on the order`,\n )\n }\n },\n})\n\n/**\n * A visitor's payment door: it opens a Stripe Checkout Session for whoever\n * calls it, so the dispatcher holds it to the card-testing counters\n * (AGL-3363). Declared here, beside the registration, so the door cannot\n * exist without it.\n */\nconst CARD_PAYMENT_DOOR = { cardPayment: true } as const\n\n/** Registers the commerce plugin's storefront API routes. */\nexport function registerCommerceApi(): void {\n registerProductCardReader()\n // The merchant's tax rule, for every plugin that prices a charge here.\n registerCommerceTaxProfile()\n registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Merchant-settable register discount ceiling (AGL-2161). Registered on\n // BOTH surfaces, like the permissions above: the POS route reads it\n // server-side, and the settings card renders it from the same schema.\n registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n // PDP/PLP template pages (AGL-418): /products/* + /collections/*.\n registerSitePageResolver(commerceSitePageResolver)\n // Seeds product grids on ordinary screens (AGL-659) — the resolver above\n // only covers /products/* and /collections/*, so /products itself, and any\n // designed page with a grid on it, needed this to render server-side.\n registerSitePageEnricher(commerceSitePageEnricher)\n registerPluginApiRoute('commerce/cart-checkout', cartCheckoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/cart', cartHandler)\n registerPluginApiRoute('commerce/catalog', catalogHandler)\n registerPluginApiRoute('commerce/checkout', checkoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/download', downloadHandler)\n registerPluginApiRoute('commerce/feed', feedHandler)\n registerPluginApiRoute('commerce/newsletter', newsletterHandler)\n registerPluginApiRoute('commerce/notify-restock', notifyRestockHandler)\n // GA-safe order projection for the storefront `purchase` (AGL-1641).\n registerPluginApiRoute('commerce/order-analytics', orderAnalyticsHandler)\n registerPluginApiRoute('commerce/product', productHandler)\n registerPluginApiRoute('commerce/related', relatedHandler)\n registerPluginApiRoute('commerce/reservation-availability', reservationAvailabilityHandler)\n registerPluginApiRoute('commerce/reserve', reserveHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/gate', gateHandler)\n registerPluginApiRoute('commerce/member-feed', memberFeedHandler)\n registerPluginApiRoute('commerce/stream', streamHandler)\n registerPluginApiRoute('commerce/subscription-portal', subscriptionPortalHandler)\n registerPluginApiRoute('commerce/reviews', reviewsHandler)\n registerPluginApiRoute('membership/account', membershipAccountHandler)\n registerPluginApiRoute('membership/content', membershipContentHandler)\n registerPluginApiRoute('membership/login', membershipLoginHandler)\n registerPluginApiRoute('membership/logout', membershipLogoutHandler)\n // Password recovery pair (AGL-552): request + complete.\n registerPluginApiRoute('membership/recover', membershipRecoverHandler)\n registerPluginApiRoute('membership/register', membershipRegisterHandler)\n registerPluginApiRoute('membership/reset', membershipResetHandler)\n registerPluginApiRoute('membership/wishlist', membershipWishlistHandler)\n}\n\n/**\n * Registers the commerce plugin's console-side API routes (AGL-396):\n * merchant/staff operations (Connect onboarding, refunds, draft & POS\n * orders, member posts), the supplier tracking callback, and the HTTP doors\n * for the abandoned-cart / restock passes.\n *\n * Those last two are NOT what schedules them — the `registerPluginJob` calls\n * above are (AGL-2227). This comment used to call them \"the scheduler-driven\n * jobs\", which is how they stayed dark for months: it asserted the wiring\n * instead of having it.\n */\nexport function registerCommerceConsoleApi(): void {\n // What a product looks like to a surface that is not this plugin's — a\n // campaign email that features one asks here rather than importing the model.\n registerProductCardReader()\n // …and why a file somebody is SELLING may not be made public (AGL-3080).\n // The media library asks before it hands an asset its permanent CDN URL\n // back; what a product is, and which of its fields hold paid media, is\n // this plugin's to know.\n registerCommerceMediaPublishGuard()\n // …and which memberships a site sells, for a security lockdown to pause\n // (AGL-3364). The lockdown route runs on this surface.\n registerCommerceRecurringCharges()\n // …and the tax rule, for the webhook that confirms what they charged.\n registerCommerceTaxProfile()\n // Stripe webhook sections (AGL-418): orders/carts/drafts/reservations/\n // subscriptions ride the platform webhook via the hook registry.\n registerBillingWebhookHandler(commerceBillingWebhookHandler)\n // Cancel + stock release in one transaction (AGL-1808). Server-side because\n // the release depends on the transition rule, and a client write could not\n // re-ask it under the same lock that flips the status.\n registerPluginApiRoute('commerce/cancel-order', cancelOrderHandler)\n registerPluginApiRoute('commerce/connect', connectHandler)\n registerPluginApiRoute('commerce/draft-order', draftOrderHandler)\n // Fulfil + mark-delivered with the transition re-asked under the write\n // (AGL-1819) — the same stale-dialog hole cancel-order closes, minus the\n // stock release those two transitions never had.\n registerPluginApiRoute('commerce/fulfill-order', fulfillOrderHandler)\n // The same transaction, offered to the HOST APP as a capability (AGL-2461)\n // so `/v1` can record a shipment without importing this library — which\n // `eslint.config.mjs`'s `scope:app` boundary forbids, and which is why the\n // public REST API could take an order and never ship it.\n //\n // Registered HERE, in the consoleApi surface, beside the route it shares an\n // implementation with: `/v1` reaches it through the console's server plugin\n // loader, the same `ensureAll(['consoleApi'])` batch the plugin API\n // dispatcher runs. A registration at module scope would instead depend on\n // which entry point a process happened to import.\n //\n // NOTHING ABOUT AUTHORIZATION CROSSES THIS LINE. The capability is a\n // pre-authorized domain operation and `/v1` does its own gating (key scope,\n // plan entitlement, org-owns-host, plugin-enabled-for-site) before it calls.\n registerOrderFulfilmentService({\n pluginId: BUNDLE_ID,\n recordShipment: recordOrderShipment,\n })\n // Issue / void store credit (AGL-2226). Server-side because the host\n // catch-all in the Firestore rules would otherwise let a client write\n // its own `balanceCents`, which checkout applies as amount-off.\n registerPluginApiRoute('commerce/gift-cards', giftCardsHandler)\n // A smart collection's rules changed or it was deleted: re-stamp which\n // products it holds (AGL-3321), the membership the storefront queries.\n registerPluginApiRoute('commerce/collection-membership', collectionMembershipHandler)\n // A site member's password help (AGL-914) and removal (AGL-3308), from the\n // Users page drawer and the Inbox. Console-auth, not the visitor cookie the\n // storefront's `membership/*` routes take, so they are served where the\n // console posts: the console's own dispatcher loads this surface only.\n registerPluginApiRoute(\n 'membership/admin-password',\n membershipAdminPasswordHandler,\n )\n registerPluginApiRoute('membership/admin-remove', membershipAdminRemoveHandler)\n registerPluginApiRoute('commerce/member-post', memberPostHandler)\n registerPluginApiRoute('commerce/pos-order', posOrderHandler)\n registerPluginApiRoute('commerce/process-abandoned', processAbandonedHandler)\n registerPluginApiRoute('commerce/process-restock', processRestockHandler)\n registerPluginApiRoute('commerce/refund', refundHandler)\n registerPluginApiRoute('commerce/supplier-update', supplierUpdateHandler)\n // The store's sales as figure tables (AGL-2915), for the AI plugin's\n // insights to read by id rather than by reading orders. The console runs\n // insight jobs, so the console surface registers them.\n registerOrderFigureReaders(() => firebaseAdmin.app().firestore())\n}\n\n// Shared with the (still app-side) membership/account route.\nexport { mintDownloadToken } from './server/download'\n\n// Site-member session primitives, shared with the (still app-side)\n// membership/* routes until those migrate too (AGL-396).\nexport * from './server/membership'\n"],"names":["registerBillingWebhookHandler","registerOrderFulfilmentService","registerPluginApiRoute","registerPluginJob","registerSitePageEnricher","registerSitePageResolver","registerPluginPermissions","registerPluginConfigSchema","isEmailConfigured","firebaseAdmin","BUNDLE_ID","registerCommerceMediaPublishGuard","registerCommerceRecurringCharges","registerProductCardReader","registerCommerceTaxProfile","commerceBillingWebhookHandler","registerOrderFigureReaders","COMMERCE_PERMISSIONS","COMMERCE_CONFIG_SCHEMA","commerceSitePageEnricher","commerceSitePageResolver","cartCheckoutHandler","cartHandler","catalogHandler","checkoutHandler","downloadHandler","feedHandler","newsletterHandler","notifyRestockHandler","productHandler","relatedHandler","reservationAvailabilityHandler","gateHandler","memberFeedHandler","membershipAccountHandler","membershipAdminPasswordHandler","membershipAdminRemoveHandler","membershipContentHandler","membershipLoginHandler","membershipLogoutHandler","membershipRecoverHandler","membershipRegisterHandler","membershipResetHandler","membershipWishlistHandler","reserveHandler","streamHandler","subscriptionPortalHandler","reviewsHandler","connectHandler","cancelOrderHandler","collectionMembershipHandler","draftOrderHandler","fulfillOrderHandler","recordOrderShipment","giftCardsHandler","memberPostHandler","orderAnalyticsHandler","posOrderHandler","processAbandonedHandler","scanAbandonedCheckouts","processRestockHandler","scanRestockAlerts","scanStockDecrements","refundHandler","scanSupplierDeliveries","supplierUpdateHandler","RECOVERY_JOB_INTERVAL_MINUTES","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","sent","console","info","scan","missingLines","truncatedHosts","warn","hosts","reportedOrders","deadLettered","CARD_PAYMENT_DOOR","cardPayment","registerCommerceApi","registerCommerceConsoleApi","recordShipment","app","firestore","mintDownloadToken"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/server.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\n\n/**\n * Server half of the commerce plugin (AGL-396): the site-facing storefront\n * API handlers, registered with the plugin API registry and served by the\n * tenant dispatcher at their unchanged `/api/commerce/*` URLs. This module\n * pulls in firebase-admin + Stripe, so it is NOT re-exported from the client\n * barrel — apps import `@aglyn/plugins-commerce/server` only from their\n * (server-only) API dispatcher registration.\n */\n\nimport {\n registerBillingWebhookHandler,\n registerOrderFulfilmentService,\n registerPluginApiRoute,\n registerPluginJob,\n registerSitePageEnricher,\n registerSitePageResolver,\n registerPluginPermissions,\n registerPluginConfigSchema,\n} from '@aglyn/aglyn/server'\nimport { isEmailConfigured } from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCommerceMediaPublishGuard } from './server/media-publish-guard'\nimport { registerCommerceRecurringCharges } from './server/recurring-charges'\nimport { registerProductCardReader } from './server/product-card'\nimport { registerCommerceTaxProfile } from './server/tax-profile'\nimport { registerTaxReturnSource } from '@aglyn/aglyn/plugin-manager/plugin-tax-return-sources'\nimport { commerceTaxReturnSource } from './server/tax-return-source'\nimport { taxSummaryHandler } from './server/tax-summary'\nimport { registerRevenueSource } from '@aglyn/aglyn/plugin-manager/plugin-revenue-sources'\nimport { commerceRevenueSource } from './server/revenue-source'\nimport { commerceBillingWebhookHandler } from './server/billing-webhook'\nimport { registerOrderFigureReaders } from './server/order-figures'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\nimport { commerceSitePageEnricher } from './server/site-page-enricher'\nimport { commerceSitePageResolver } from './server/site-page-resolver'\nimport { cartCheckoutHandler } from './server/cart-checkout'\nimport { cartHandler } from './server/cart'\nimport { catalogHandler } from './server/catalog'\nimport { checkoutHandler } from './server/checkout'\nimport { downloadHandler } from './server/download'\nimport { feedHandler } from './server/feed'\nimport { newsletterHandler } from './server/newsletter'\nimport { notifyRestockHandler } from './server/notify-restock'\nimport { productHandler } from './server/product'\nimport { relatedHandler } from './server/related'\nimport { reservationAvailabilityHandler } from './server/reservation-availability'\nimport { gateHandler } from './server/gate'\nimport { memberFeedHandler } from './server/member-feed'\nimport { membershipAccountHandler } from './server/membership-account'\nimport { membershipAdminPasswordHandler } from './server/membership-admin-password'\nimport { membershipAdminRemoveHandler } from './server/membership-admin-remove'\nimport { membershipContentHandler } from './server/membership-content'\nimport { membershipLoginHandler } from './server/membership-login'\nimport { membershipLogoutHandler } from './server/membership-logout'\nimport { membershipRecoverHandler } from './server/membership-recover'\nimport { membershipRegisterHandler } from './server/membership-register'\nimport { membershipResetHandler } from './server/membership-reset'\nimport { membershipWishlistHandler } from './server/membership-wishlist'\nimport { reserveHandler } from './server/reserve'\nimport { streamHandler } from './server/stream'\nimport { subscriptionPortalHandler } from './server/subscription-portal'\nimport { reviewsHandler } from './server/reviews'\nimport { connectHandler } from './server/connect'\nimport { cancelOrderHandler } from './server/cancel-order'\nimport { collectionMembershipHandler } from './server/collection-membership'\nimport { draftOrderHandler } from './server/draft-order'\nimport {\n fulfillOrderHandler,\n recordOrderShipment,\n} from './server/fulfill-order'\nimport { giftCardsHandler } from './server/gift-cards'\nimport { memberPostHandler } from './server/member-post'\nimport { orderAnalyticsHandler } from './server/order-analytics'\nimport { posOrderHandler } from './server/pos-order'\nimport {\n processAbandonedHandler,\n scanAbandonedCheckouts,\n} from './server/process-abandoned'\nimport { processRestockHandler, scanRestockAlerts } from './server/process-restock'\nimport { scanStockDecrements } from './server/reconcile-stock'\nimport { refundHandler } from './server/refund'\nimport { scanSupplierDeliveries } from './server/supplier-outbox'\nimport { supplierUpdateHandler } from './server/supplier-update'\n\n/**\n * The two commerce beats (AGL-2227).\n *\n * `commerce/process-abandoned` and `commerce/process-restock` have existed\n * since AGL-323/326 as `x-cron-secret` HTTP doors, and `registerCommerceConsoleApi`\n * below has called them \"the scheduler-driven jobs\" in a comment that whole\n * time. **Nothing scheduled them.** Not `scheduled-crons.yml` (its table names\n * 11 paths; neither of these), not `vercel.json` (no `crons` key at all), not\n * `registerPluginJob` — the only two registrations in the repo were scheduled\n * publishing and the bookings hold-expiry.\n *\n * The cost was a Pro-tier entitlement (`abandonedCart`) that had never sent an\n * email, and a storefront \"notify me when it's back\" form writing into a queue\n * with no drain. AGL-1793 had already added the collection-group indexes these\n * two scans need, which is the clearest evidence they were always meant to run.\n *\n * Module scope, like the bookings job beside it: the runner route reaches jobs\n * through `ensureAll(['tenantApi'])`, and a registration inside a `register*`\n * function would depend on which entry point happened to be loaded.\n *\n * 15 minutes, not 1: `process-abandoned` will not remind a checkout younger\n * than an hour and `process-restock` only mails alerts whose product already\n * has stock, so a tighter beat buys nothing and costs two collection-group\n * scans a minute. Both are bounded (200 docs) and idempotent — each pass\n * stamps what it sent — so an overlapping or repeated beat cannot double-send.\n */\nconst RECOVERY_JOB_INTERVAL_MINUTES = 15\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'abandoned-checkout-recovery',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description:\n 'Email one recovery reminder per stalled checkout (abandonedCart plans).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n // Quietly, not as an error: email is optional per deployment, and a beat\n // that logs every minute on a self-host without Resend buries everything\n // else in the log.\n if (!isEmailConfigured()) return\n const { sent } = await scanAbandonedCheckouts(gate)\n if (sent) console.info(`commerce: sent ${sent} abandoned-cart reminders`)\n },\n})\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'back-in-stock-alerts',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description: 'Email shoppers whose requested product is in stock again.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n if (!isEmailConfigured()) return\n const { sent } = await scanRestockAlerts(gate)\n if (sent) console.info(`commerce: sent ${sent} back-in-stock alerts`)\n },\n})\n\n/**\n * The missing-decrement detector (AGL-2358).\n *\n * HOURLY, not on the 15-minute recovery beat: it is a detector for a rare\n * process death, not a queue drain, and nothing it finds gets less true for\n * waiting an hour. The cost of the beat is what sets the interval — one\n * collection-group read of the platform's most recent orders, plus two\n * queries per host that has any — and an hour keeps that off the same tick as\n * the two scans above.\n *\n * NOT gated on `isEmailConfigured()`, unlike its two neighbours. Those send\n * mail; this writes a console notification and a log line, both of which work\n * on a self-host with no mail provider at all — and a stock count that is\n * silently wrong is exactly what a self-hoster least wants suppressed by a\n * setting about email.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'stock-decrement-reconciliation',\n intervalMinutes: 60,\n description:\n 'Report paid orders whose stock decrement never landed (AGL-2358).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanStockDecrements(gate)\n if (scan.missingLines || scan.truncatedHosts) {\n console.warn(\n `commerce: ${scan.missingLines} order lines across ${scan.hosts} ` +\n `sites have no sale ledger row (${scan.reportedOrders} newly ` +\n `reported, ${scan.truncatedHosts} sites' ledger window truncated)`,\n )\n }\n },\n})\n\n/**\n * The dropship supplier outbox drain (AGL-2473).\n *\n * EVERY MINUTE, unlike the three above, and the interval is the point rather\n * than an oversight. This is the only one of the four that a BUYER is waiting\n * on: the row it drains is a paid order that has not been routed to whoever\n * ships it, and every minute it sits is a minute the parcel is not moving. The\n * first backoff step is also 60s, so a supplier that blipped is retried on the\n * next tick.\n *\n * The cost that set fifteen minutes for the recovery pair does not apply. Those\n * are collection-GROUP scans across every site on the platform; this is one\n * ordinary equality query against a top-level collection that is EMPTY in the\n * ordinary case — an empty result bills a single read, so the whole beat is\n * ~1,440 reads a day whether or not anyone is dropshipping.\n *\n * Not gated on `isEmailConfigured()`. The supplier notification is an HTTP POST\n * to the merchant's own supplier, and a self-host with no mail provider still\n * has dropship orders to route.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'supplier-webhook-delivery',\n intervalMinutes: 1,\n description:\n 'Deliver queued dropship supplier notifications, with backoff and a ' +\n 'dead letter that tells the merchant (AGL-2473).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanSupplierDeliveries(gate)\n if (scan.deadLettered) {\n console.warn(\n `commerce: ${scan.deadLettered} dropship orders could not be routed ` +\n `to their supplier and have been reported on the order`,\n )\n }\n },\n})\n\n/**\n * A visitor's payment door: it opens a Stripe Checkout Session for whoever\n * calls it, so the dispatcher holds it to the card-testing counters\n * (AGL-3363). Declared here, beside the registration, so the door cannot\n * exist without it.\n */\nconst CARD_PAYMENT_DOOR = { cardPayment: true } as const\n\n/** Registers the commerce plugin's storefront API routes. */\nexport function registerCommerceApi(): void {\n registerProductCardReader()\n // The merchant's tax rule, for every plugin that prices a charge here.\n registerCommerceTaxProfile()\n registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Merchant-settable register discount ceiling (AGL-2161). Registered on\n // BOTH surfaces, like the permissions above: the POS route reads it\n // server-side, and the settings card renders it from the same schema.\n registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n // PDP/PLP template pages (AGL-418): /products/* + /collections/*.\n registerSitePageResolver(commerceSitePageResolver)\n // Seeds product grids on ordinary screens (AGL-659) — the resolver above\n // only covers /products/* and /collections/*, so /products itself, and any\n // designed page with a grid on it, needed this to render server-side.\n registerSitePageEnricher(commerceSitePageEnricher)\n registerPluginApiRoute('commerce/cart-checkout', cartCheckoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/cart', cartHandler)\n registerPluginApiRoute('commerce/catalog', catalogHandler)\n registerPluginApiRoute('commerce/checkout', checkoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/download', downloadHandler)\n registerPluginApiRoute('commerce/feed', feedHandler)\n registerPluginApiRoute('commerce/newsletter', newsletterHandler)\n registerPluginApiRoute('commerce/notify-restock', notifyRestockHandler)\n // GA-safe order projection for the storefront `purchase` (AGL-1641).\n registerPluginApiRoute('commerce/order-analytics', orderAnalyticsHandler)\n registerPluginApiRoute('commerce/product', productHandler)\n registerPluginApiRoute('commerce/related', relatedHandler)\n registerPluginApiRoute('commerce/reservation-availability', reservationAvailabilityHandler)\n registerPluginApiRoute('commerce/reserve', reserveHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/gate', gateHandler)\n registerPluginApiRoute('commerce/member-feed', memberFeedHandler)\n registerPluginApiRoute('commerce/stream', streamHandler)\n registerPluginApiRoute('commerce/subscription-portal', subscriptionPortalHandler)\n registerPluginApiRoute('commerce/reviews', reviewsHandler)\n registerPluginApiRoute('membership/account', membershipAccountHandler)\n registerPluginApiRoute('membership/content', membershipContentHandler)\n registerPluginApiRoute('membership/login', membershipLoginHandler)\n registerPluginApiRoute('membership/logout', membershipLogoutHandler)\n // Password recovery pair (AGL-552): request + complete.\n registerPluginApiRoute('membership/recover', membershipRecoverHandler)\n registerPluginApiRoute('membership/register', membershipRegisterHandler)\n registerPluginApiRoute('membership/reset', membershipResetHandler)\n registerPluginApiRoute('membership/wishlist', membershipWishlistHandler)\n}\n\n/**\n * Registers the commerce plugin's console-side API routes (AGL-396):\n * merchant/staff operations (Connect onboarding, refunds, draft & POS\n * orders, member posts), the supplier tracking callback, and the HTTP doors\n * for the abandoned-cart / restock passes.\n *\n * Those last two are NOT what schedules them — the `registerPluginJob` calls\n * above are (AGL-2227). This comment used to call them \"the scheduler-driven\n * jobs\", which is how they stayed dark for months: it asserted the wiring\n * instead of having it.\n */\nexport function registerCommerceConsoleApi(): void {\n // What a product looks like to a surface that is not this plugin's — a\n // campaign email that features one asks here rather than importing the model.\n registerProductCardReader()\n // …and why a file somebody is SELLING may not be made public (AGL-3080).\n // The media library asks before it hands an asset its permanent CDN URL\n // back; what a product is, and which of its fields hold paid media, is\n // this plugin's to know.\n registerCommerceMediaPublishGuard()\n // …and which memberships a site sells, for a security lockdown to pause\n // (AGL-3364). The lockdown route runs on this surface.\n registerCommerceRecurringCharges()\n // …and the tax rule, for the webhook that confirms what they charged.\n registerCommerceTaxProfile()\n // …and its sales on the operator's own sales tax return (AGL-3080): the\n // tax a storefront checkout collected under the platform's registrations.\n // The staff return awaits this surface before it asks, and refuses to be\n // filed without this source — commerce declares `taxReturnSource`.\n registerTaxReturnSource(commerceTaxReturnSource, { pluginId: BUNDLE_ID })\n // …and the take its orders earn the operator, on the staff revenue report\n // (AGL-3080) — commerce declares `revenueSource`, so the report names this\n // source as unread rather than counting it as zero if it is missing.\n registerRevenueSource(commerceRevenueSource, { pluginId: BUNDLE_ID })\n // Stripe webhook sections (AGL-418): orders/carts/drafts/reservations/\n // subscriptions ride the platform webhook via the hook registry.\n registerBillingWebhookHandler(commerceBillingWebhookHandler)\n // Cancel + stock release in one transaction (AGL-1808). Server-side because\n // the release depends on the transition rule, and a client write could not\n // re-ask it under the same lock that flips the status.\n registerPluginApiRoute('commerce/cancel-order', cancelOrderHandler)\n registerPluginApiRoute('commerce/connect', connectHandler)\n registerPluginApiRoute('commerce/draft-order', draftOrderHandler)\n // Fulfil + mark-delivered with the transition re-asked under the write\n // (AGL-1819) — the same stale-dialog hole cancel-order closes, minus the\n // stock release those two transitions never had.\n registerPluginApiRoute('commerce/fulfill-order', fulfillOrderHandler)\n // The same transaction, offered to the HOST APP as a capability (AGL-2461)\n // so `/v1` can record a shipment without importing this library — which\n // `eslint.config.mjs`'s `scope:app` boundary forbids, and which is why the\n // public REST API could take an order and never ship it.\n //\n // Registered HERE, in the consoleApi surface, beside the route it shares an\n // implementation with: `/v1` reaches it through the console's server plugin\n // loader, the same `ensureAll(['consoleApi'])` batch the plugin API\n // dispatcher runs. A registration at module scope would instead depend on\n // which entry point a process happened to import.\n //\n // NOTHING ABOUT AUTHORIZATION CROSSES THIS LINE. The capability is a\n // pre-authorized domain operation and `/v1` does its own gating (key scope,\n // plan entitlement, org-owns-host, plugin-enabled-for-site) before it calls.\n registerOrderFulfilmentService({\n pluginId: BUNDLE_ID,\n recordShipment: recordOrderShipment,\n })\n // Issue / void store credit (AGL-2226). Server-side because the host\n // catch-all in the Firestore rules would otherwise let a client write\n // its own `balanceCents`, which checkout applies as amount-off.\n registerPluginApiRoute('commerce/gift-cards', giftCardsHandler)\n // The merchant's own storefront sales tax, by who owes it (AGL-2440): the\n // same rows and classifier the operator's return reads, fenced to one site.\n registerPluginApiRoute('commerce/tax-summary', { web: taxSummaryHandler })\n // A smart collection's rules changed or it was deleted: re-stamp which\n // products it holds (AGL-3321), the membership the storefront queries.\n registerPluginApiRoute('commerce/collection-membership', collectionMembershipHandler)\n // A site member's password help (AGL-914) and removal (AGL-3308), from the\n // Users page drawer and the Inbox. Console-auth, not the visitor cookie the\n // storefront's `membership/*` routes take, so they are served where the\n // console posts: the console's own dispatcher loads this surface only.\n registerPluginApiRoute(\n 'membership/admin-password',\n membershipAdminPasswordHandler,\n )\n registerPluginApiRoute('membership/admin-remove', membershipAdminRemoveHandler)\n registerPluginApiRoute('commerce/member-post', memberPostHandler)\n registerPluginApiRoute('commerce/pos-order', posOrderHandler)\n registerPluginApiRoute('commerce/process-abandoned', processAbandonedHandler)\n registerPluginApiRoute('commerce/process-restock', processRestockHandler)\n registerPluginApiRoute('commerce/refund', refundHandler)\n registerPluginApiRoute('commerce/supplier-update', supplierUpdateHandler)\n // The store's sales as figure tables (AGL-2915), for the AI plugin's\n // insights to read by id rather than by reading orders. The console runs\n // insight jobs, so the console surface registers them.\n registerOrderFigureReaders(() => firebaseAdmin.app().firestore())\n}\n\n// Shared with the (still app-side) membership/account route.\nexport { mintDownloadToken } from './server/download'\n\n// Site-member session primitives, shared with the (still app-side)\n// membership/* routes until those migrate too (AGL-396).\nexport * from './server/membership'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { commercePlanEntitlements } from './plan-entitlements'\n"],"names":["registerBillingWebhookHandler","registerOrderFulfilmentService","registerPluginApiRoute","registerPluginJob","registerSitePageEnricher","registerSitePageResolver","registerPluginPermissions","registerPluginConfigSchema","isEmailConfigured","firebaseAdmin","BUNDLE_ID","registerCommerceMediaPublishGuard","registerCommerceRecurringCharges","registerProductCardReader","registerCommerceTaxProfile","registerTaxReturnSource","commerceTaxReturnSource","taxSummaryHandler","registerRevenueSource","commerceRevenueSource","commerceBillingWebhookHandler","registerOrderFigureReaders","COMMERCE_PERMISSIONS","COMMERCE_CONFIG_SCHEMA","commerceSitePageEnricher","commerceSitePageResolver","cartCheckoutHandler","cartHandler","catalogHandler","checkoutHandler","downloadHandler","feedHandler","newsletterHandler","notifyRestockHandler","productHandler","relatedHandler","reservationAvailabilityHandler","gateHandler","memberFeedHandler","membershipAccountHandler","membershipAdminPasswordHandler","membershipAdminRemoveHandler","membershipContentHandler","membershipLoginHandler","membershipLogoutHandler","membershipRecoverHandler","membershipRegisterHandler","membershipResetHandler","membershipWishlistHandler","reserveHandler","streamHandler","subscriptionPortalHandler","reviewsHandler","connectHandler","cancelOrderHandler","collectionMembershipHandler","draftOrderHandler","fulfillOrderHandler","recordOrderShipment","giftCardsHandler","memberPostHandler","orderAnalyticsHandler","posOrderHandler","processAbandonedHandler","scanAbandonedCheckouts","processRestockHandler","scanRestockAlerts","scanStockDecrements","refundHandler","scanSupplierDeliveries","supplierUpdateHandler","RECOVERY_JOB_INTERVAL_MINUTES","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","sent","console","info","scan","missingLines","truncatedHosts","warn","hosts","reportedOrders","deadLettered","CARD_PAYMENT_DOOR","cardPayment","registerCommerceApi","registerCommerceConsoleApi","recordShipment","web","app","firestore","mintDownloadToken"],"mappings":"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"}
@@ -0,0 +1,19 @@
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 StarterTemplate } from '@aglyn/aglyn/app-utils/starter-template-nodes';
18
+ /** This plugin's starters, as the manifest generator compiles them. */
19
+ export declare function commerceStarterTemplates(): StarterTemplate[];