@aglyn/plugins-commerce 1.0.0-beta.217 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +15 -15
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -19
- package/src/index.js.map +1 -1
- package/src/lib/components/cart.js +2 -1
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/console/catalog-organization-card.component.js +44 -18
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +2 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/member-posts-card.component.js +1 -1
- package/src/lib/components/console/member-posts-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +8 -7
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.js +114 -63
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +184 -0
- package/src/lib/components/console/product-zones.js +54 -0
- package/src/lib/components/console/product-zones.js.map +1 -0
- package/src/lib/components/console/products-hub-card.component.js +3 -2
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +1 -1
- package/src/lib/components/console/products-hub-zone.component.js +2 -1
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/store-settings-card.component.js +2 -2
- package/src/lib/components/console/store-settings-card.component.js.map +1 -1
- package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -1
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
- package/src/lib/components/product-detail.js +2 -1
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-grid.js +1 -1
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/components/storefront-payment-element.js +36 -5
- package/src/lib/components/storefront-payment-element.js.map +1 -1
- package/src/lib/constants/storefront-payments.d.ts +33 -0
- package/src/lib/constants/storefront-payments.js +35 -0
- package/src/lib/constants/storefront-payments.js.map +1 -0
- package/src/lib/declarations.server.d.ts +3 -1
- package/src/lib/declarations.server.js +19 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/commerce-record-routes.d.ts +27 -0
- package/src/lib/model/commerce-record-routes.js +72 -0
- package/src/lib/model/commerce-record-routes.js.map +1 -0
- package/src/lib/model/commerce-search-sources.d.ts +21 -0
- package/src/lib/model/commerce-search-sources.js +40 -0
- package/src/lib/model/commerce-search-sources.js.map +1 -0
- package/src/lib/model/storefront-tax-summary.d.ts +121 -0
- package/src/lib/model/storefront-tax-summary.js +148 -0
- package/src/lib/model/storefront-tax-summary.js.map +1 -0
- package/src/lib/plan-entitlements.d.ts +35 -0
- package/src/lib/plan-entitlements.js +54 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +9 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +135 -168
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +2 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/checkout.js +2 -2
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/connect.js +5 -5
- package/src/lib/server/connect.js.map +1 -1
- package/src/lib/server/draft-order.js +2 -2
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/membership-content.js +1 -1
- package/src/lib/server/membership-content.js.map +1 -1
- package/src/lib/server/native-checkout.d.ts +20 -5
- package/src/lib/server/native-checkout.js +27 -6
- package/src/lib/server/native-checkout.js.map +1 -1
- package/src/lib/server/pos-order.js +2 -2
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/product-record-index.d.ts +19 -0
- package/src/lib/server/product-record-index.js +115 -0
- package/src/lib/server/product-record-index.js.map +1 -0
- package/src/lib/server/reserve.js +2 -2
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/revenue-source.d.ts +130 -0
- package/src/lib/server/revenue-source.js +286 -0
- package/src/lib/server/revenue-source.js.map +1 -0
- package/src/lib/server/tax-profile.d.ts +8 -5
- package/src/lib/server/tax-profile.js +22 -5
- package/src/lib/server/tax-profile.js.map +1 -1
- package/src/lib/server/tax-return-source.d.ts +125 -0
- package/src/lib/server/tax-return-source.js +524 -0
- package/src/lib/server/tax-return-source.js.map +1 -0
- package/src/lib/server/tax-summary.d.ts +92 -0
- package/src/lib/server/tax-summary.js +258 -0
- package/src/lib/server/tax-summary.js.map +1 -0
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js +23 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.d.ts +33 -0
- package/src/lib/tenant-emails.js +511 -0
- package/src/lib/tenant-emails.js.map +1 -0
- package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
- package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
- package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
|
@@ -0,0 +1,286 @@
|
|
|
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
|
+
*/ /**
|
|
18
|
+
* The storefront's take on the operator's revenue report (AGL-2486, AGL-3080).
|
|
19
|
+
*
|
|
20
|
+
* Answered through the platform's revenue source contract
|
|
21
|
+
* (`@aglyn/aglyn/plugin-manager/plugin-revenue-sources`). The report folds the
|
|
22
|
+
* operator's own invoices; this is what storefront orders earned it — and the
|
|
23
|
+
* one figure on that page most likely to be reported wrongly.
|
|
24
|
+
*
|
|
25
|
+
* An order's `feeCents` is the Connect `application_fee_amount`, which since
|
|
26
|
+
* AGL-2152 is the advertised take PLUS Stripe's processing cost:
|
|
27
|
+
*
|
|
28
|
+
* fee = take%(goods) + processing%(charge) + 30¢
|
|
29
|
+
*
|
|
30
|
+
* Every storefront charge is a DESTINATION charge, so Stripe moves the whole
|
|
31
|
+
* amount to the merchant and debits its processing fee from the PLATFORM's
|
|
32
|
+
* balance; the pass-through half of that fee exists purely to recover a cost
|
|
33
|
+
* the platform has already paid. Reporting `feeCents` as earnings would
|
|
34
|
+
* overstate the margin on every single storefront sale — and on a small order
|
|
35
|
+
* it would report the 30¢ Stripe just took as money the platform made.
|
|
36
|
+
*
|
|
37
|
+
* So the pass-through is recomputed with the SAME helper the fee was charged
|
|
38
|
+
* from (`storefrontProcessingCostCents`) and subtracted. Both halves are
|
|
39
|
+
* reported, because "we collected X of which Y was Stripe's" is the sentence a
|
|
40
|
+
* margin figure needs.
|
|
41
|
+
*
|
|
42
|
+
* The recomputation is an estimate in one direction only: it re-derives the
|
|
43
|
+
* cost from the stored charge amount rather than reading Stripe's actual
|
|
44
|
+
* balance-transaction fee, and it prices it at the DEAREST enabled payment
|
|
45
|
+
* method's rate (BNPL, 6%) because that is the rate the fee was charged at.
|
|
46
|
+
* A card-family order really costs 2.9%, so on those orders this subtracts
|
|
47
|
+
* more than Stripe took and the take is UNDERSTATED — the safe direction, and
|
|
48
|
+
* stated on the page rather than left to be discovered.
|
|
49
|
+
*
|
|
50
|
+
* ## Subscription cycles, and the one cycle this understates
|
|
51
|
+
*
|
|
52
|
+
* A storefront SUBSCRIPTION cycle carries a `subscriptionId`. Since AGL-2655
|
|
53
|
+
* its fee carries the same pass-through, folded into
|
|
54
|
+
* `application_fee_percent` as a rate because a Stripe subscription cannot
|
|
55
|
+
* express a fixed 30¢ — so it is netted out here exactly as a one-time sale's
|
|
56
|
+
* is. The percent is sized on the recurring goods and the recomputed cost here
|
|
57
|
+
* is sized on the whole amount paid, tax included, so a taxed cycle subtracts
|
|
58
|
+
* slightly more than was recovered: understated, the safe direction.
|
|
59
|
+
*
|
|
60
|
+
* A subscription sold before the pass-through existed is carried onto it by
|
|
61
|
+
* the renewal re-price (AGL-2289) at its next paid invoice — but THAT invoice
|
|
62
|
+
* was billed at the bare take, so its row's `feeCents` holds no pass-through
|
|
63
|
+
* and the clamp reports its take as zero. One cycle per legacy subscription,
|
|
64
|
+
* and the count beside the figure is how a reader sees how much of the book
|
|
65
|
+
* that is.
|
|
66
|
+
*/ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
67
|
+
import { storefrontProcessingCostCents } from "@aglyn/aglyn/server";
|
|
68
|
+
import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
|
|
69
|
+
import { stripeIdIsTestMode } from "@aglyn/aglyn/app-utils/stripe-deployment-mode";
|
|
70
|
+
import { groupRevenueAttribution } from "@aglyn/aglyn/plugin-manager/plugin-revenue-sources";
|
|
71
|
+
/** Cents from anything, defaulting to 0 rather than NaN. */ function cents(value) {
|
|
72
|
+
const parsed = Number(value != null ? value : 0);
|
|
73
|
+
return Number.isFinite(parsed) ? Math.round(parsed) : 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether this order row was a test-mode rehearsal.
|
|
77
|
+
*
|
|
78
|
+
* A recorded `livemode` wins, and the session id's prefix is the fallback for
|
|
79
|
+
* rows written before anything recorded it. An order with neither signal is
|
|
80
|
+
* LIVE: a POS cash sale carries no Stripe session at all, and answering "test"
|
|
81
|
+
* for anything unidentifiable would erase genuine revenue from the report.
|
|
82
|
+
*/ function isTestModeOrderRow(row) {
|
|
83
|
+
var _row_checkoutSessionId;
|
|
84
|
+
if (typeof row.livemode === 'boolean') return !row.livemode;
|
|
85
|
+
return stripeIdIsTestMode((_row_checkoutSessionId = row.checkoutSessionId) != null ? _row_checkoutSessionId : row.id);
|
|
86
|
+
}
|
|
87
|
+
export function commerceSettledSummary(rows, truncated = false) {
|
|
88
|
+
const out = {
|
|
89
|
+
transactionCount: 0,
|
|
90
|
+
grossCents: 0,
|
|
91
|
+
applicationFeeCents: 0,
|
|
92
|
+
processingPassThroughCents: 0,
|
|
93
|
+
commissionCents: 0,
|
|
94
|
+
commissionRefundedCents: 0,
|
|
95
|
+
commissionNetCents: 0,
|
|
96
|
+
subscriptionOrders: 0,
|
|
97
|
+
truncated: truncated === true
|
|
98
|
+
};
|
|
99
|
+
for (const row of rows != null ? rows : []){
|
|
100
|
+
// A REHEARSAL IS NOT REVENUE. A smoke-test checkout writes a real order
|
|
101
|
+
// document — Stripe never moved money for it, and its session id says so.
|
|
102
|
+
// Skipped ENTIRELY rather than counted at zero, because `transactionCount`
|
|
103
|
+
// is read as "how many sales", and a rehearsal is not one.
|
|
104
|
+
if (isTestModeOrderRow(row)) continue;
|
|
105
|
+
const split = commerceOrderTake(row);
|
|
106
|
+
out.transactionCount += 1;
|
|
107
|
+
out.grossCents += split.gross;
|
|
108
|
+
out.applicationFeeCents += split.fee;
|
|
109
|
+
if (split.isSubscriptionOrder) out.subscriptionOrders += 1;
|
|
110
|
+
out.processingPassThroughCents += split.passThrough;
|
|
111
|
+
out.commissionCents += split.take;
|
|
112
|
+
out.commissionRefundedCents += split.takeRefunded;
|
|
113
|
+
out.commissionNetCents += split.takeNet;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One storefront order's take, in ONE place.
|
|
119
|
+
*
|
|
120
|
+
* Shared with the per-host attribution so a host table cannot sum to
|
|
121
|
+
* something other than the storefront line above it.
|
|
122
|
+
*
|
|
123
|
+
* A zero-fee order contributes its gross and its count and nothing else —
|
|
124
|
+
* there is no take to attribute, and inventing one from the gross would
|
|
125
|
+
* report a merchant's money as the platform's.
|
|
126
|
+
*/ export function commerceOrderTake(row) {
|
|
127
|
+
const gross = cents(row == null ? void 0 : row.amountCents);
|
|
128
|
+
const fee = Math.max(0, cents(row == null ? void 0 : row.feeCents));
|
|
129
|
+
const isSubscriptionOrder = typeof (row == null ? void 0 : row.subscriptionId) === 'string' && row.subscriptionId.length > 0;
|
|
130
|
+
if (fee <= 0) {
|
|
131
|
+
return {
|
|
132
|
+
gross,
|
|
133
|
+
fee,
|
|
134
|
+
isSubscriptionOrder,
|
|
135
|
+
passThrough: 0,
|
|
136
|
+
take: 0,
|
|
137
|
+
takeRefunded: 0,
|
|
138
|
+
takeNet: 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Every fee bundles the recovery — a one-time sale's in cents, a
|
|
142
|
+
// subscription cycle's as a rate (AGL-2655). Clamped to the fee itself: the
|
|
143
|
+
// recomputed cost can exceed a fee charged under an older rate, and a
|
|
144
|
+
// negative take would subtract from another order's real margin.
|
|
145
|
+
const passThrough = Math.min(fee, storefrontProcessingCostCents(gross));
|
|
146
|
+
const take = fee - passThrough;
|
|
147
|
+
const refunded = Math.min(gross, Math.max(0, cents(row == null ? void 0 : row.refundedCents)));
|
|
148
|
+
const takeRefunded = gross > 0 ? Math.round(refunded * take / gross) : 0;
|
|
149
|
+
return {
|
|
150
|
+
gross,
|
|
151
|
+
fee,
|
|
152
|
+
isSubscriptionOrder,
|
|
153
|
+
passThrough,
|
|
154
|
+
take,
|
|
155
|
+
takeRefunded,
|
|
156
|
+
takeNet: take - takeRefunded
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Storefront take by HOST.
|
|
161
|
+
*
|
|
162
|
+
* By host and not by org on purpose: one org can run several storefronts, and
|
|
163
|
+
* "which store earns" is the question a per-org roll-up destroys.
|
|
164
|
+
*/ export function commerceHostAttribution(rows, limit = 100) {
|
|
165
|
+
return groupRevenueAttribution((rows != null ? rows : []).map((row)=>{
|
|
166
|
+
var _ref;
|
|
167
|
+
const split = commerceOrderTake(row);
|
|
168
|
+
return {
|
|
169
|
+
key: String((_ref = row == null ? void 0 : row.hostId) != null ? _ref : ''),
|
|
170
|
+
detail: '',
|
|
171
|
+
gain: split.takeNet,
|
|
172
|
+
loss: split.takeRefunded
|
|
173
|
+
};
|
|
174
|
+
}), limit, 'Host not recorded');
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The storefront's answer for one period, from figures already folded. Pure,
|
|
178
|
+
* so what the report prints can be proved from fixtures.
|
|
179
|
+
*
|
|
180
|
+
* `failed` is a sweep that could not run — a collection-group index this
|
|
181
|
+
* deployment has not built answers FAILED_PRECONDITION — and it is reported as
|
|
182
|
+
* a failure with figures of zero, never as a cap: "we read part of it" and "we
|
|
183
|
+
* read none of it" have different remedies.
|
|
184
|
+
*/ export function commerceRevenueAnswer(read) {
|
|
185
|
+
var _settled_commissionNetCents, _settled_subscriptionOrders;
|
|
186
|
+
const brand = PLATFORM_BRAND_NAME;
|
|
187
|
+
const { settled } = read;
|
|
188
|
+
return {
|
|
189
|
+
id: 'commerce',
|
|
190
|
+
name: 'storefront orders',
|
|
191
|
+
earned: {
|
|
192
|
+
label: 'Storefront commission',
|
|
193
|
+
cents: Number((_settled_commissionNetCents = settled.commissionNetCents) != null ? _settled_commissionNetCents : 0),
|
|
194
|
+
note: 'The advertised take on merchant storefront sales, net of refunds — ' + 'with Stripe’s card processing removed. The platform fee charged on a ' + 'storefront sale bundles that processing cost and passes it through at ' + 'cost; it is a recovery, not earnings.'
|
|
195
|
+
},
|
|
196
|
+
grossToNet: [
|
|
197
|
+
{
|
|
198
|
+
label: 'Storefront sales (shopper gross)',
|
|
199
|
+
cents: settled.grossCents,
|
|
200
|
+
deduction: false,
|
|
201
|
+
note: 'The merchant’s. It transfers straight to their connected account ' + 'and is shown only for scale.'
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
label: 'Storefront platform fee collected',
|
|
205
|
+
cents: settled.applicationFeeCents,
|
|
206
|
+
deduction: false,
|
|
207
|
+
note: `Not all ${brand}’s — see the next line.`
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
label: '— less card processing passed through at cost',
|
|
211
|
+
cents: settled.processingPassThroughCents,
|
|
212
|
+
deduction: true,
|
|
213
|
+
note: `Stripe’s. On a destination charge Stripe debits ${brand}’s balance for processing, and this half of the fee recovers exactly that. It is a recovery, not earnings, and reporting it as revenue would overstate every storefront sale.`
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
notes: Number((_settled_subscriptionOrders = settled.subscriptionOrders) != null ? _settled_subscriptionOrders : 0) > 0 ? [
|
|
217
|
+
{
|
|
218
|
+
label: `${settled.subscriptionOrders} storefront subscription cycles — one billed before its re-price reports no take`,
|
|
219
|
+
tone: 'warning'
|
|
220
|
+
}
|
|
221
|
+
] : [],
|
|
222
|
+
attribution: [
|
|
223
|
+
_extends({
|
|
224
|
+
id: 'byHost',
|
|
225
|
+
heading: 'Storefront take by host',
|
|
226
|
+
unit: 'Storefront',
|
|
227
|
+
countLabel: 'Orders',
|
|
228
|
+
empty: `No storefront order settled in this period. Note this is ${brand}'s take only — the shopper's spend is the merchant's money and is never counted here.`,
|
|
229
|
+
chartEmpty: 'No storefront earned a take in this period — nothing to plot yet.'
|
|
230
|
+
}, read.byHost)
|
|
231
|
+
],
|
|
232
|
+
attributionNote: "Storefront figures above are the advertised take with Stripe's " + 'processing cost already subtracted, on the basis stated in “Gross ' + 'versus net” below. Attribution is by host rather than by org because ' + 'one org can run several storefronts, and rolling them up destroys the ' + 'question.',
|
|
233
|
+
truncated: settled.truncated,
|
|
234
|
+
failure: read.failed ? {
|
|
235
|
+
title: 'Storefront orders could not be read',
|
|
236
|
+
detail: 'The storefront commission below reads $0 because the query ' + 'failed, not because there were no sales. The sweep needs the ' + 'COLLECTION_GROUP index on orders.createdAtMs — check it is still ' + 'declared in the Firestore index config and actually deployed, ' + 'since indexes ship separately from the app.'
|
|
237
|
+
} : null,
|
|
238
|
+
summary: settled
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The storefront's source, registered on the console's API surface: the
|
|
243
|
+
* staff revenue report awaits that surface before it asks.
|
|
244
|
+
*/ export const commerceRevenueSource = {
|
|
245
|
+
async read (request) {
|
|
246
|
+
var _ref;
|
|
247
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
248
|
+
// Storefront orders live per host (`hosts/{hostId}/orders`), so this is a
|
|
249
|
+
// collection-GROUP range — ranged on `createdAtMs`, the NUMBER, and not on
|
|
250
|
+
// the `createdAt` Timestamp beside it. A collection-group range needs a
|
|
251
|
+
// COLLECTION_GROUP-scoped single-field index — Firestore's automatic ones
|
|
252
|
+
// are COLLECTION scope only — and the index that is declared and deployed
|
|
253
|
+
// is on `createdAtMs` (AGL-1793). The `createdAt` form answers
|
|
254
|
+
// FAILED_PRECONDITION on every request, which is how the report once read
|
|
255
|
+
// $0 for every storefront.
|
|
256
|
+
let swept;
|
|
257
|
+
try {
|
|
258
|
+
swept = await request.sweep(firestore.collectionGroup('orders').where('createdAtMs', '>=', request.start.getTime()).where('createdAtMs', '<', request.end.getTime()), 'createdAtMs');
|
|
259
|
+
} catch (unused) {
|
|
260
|
+
swept = null;
|
|
261
|
+
}
|
|
262
|
+
const rows = ((_ref = swept == null ? void 0 : swept.docs) != null ? _ref : []).map((doc)=>{
|
|
263
|
+
var _ref;
|
|
264
|
+
var _doc_ref_parent_parent;
|
|
265
|
+
return _extends({
|
|
266
|
+
id: doc.id,
|
|
267
|
+
// The storefront that earned the take is in the PATH.
|
|
268
|
+
hostId: (_ref = (_doc_ref_parent_parent = doc.ref.parent.parent) == null ? void 0 : _doc_ref_parent_parent.id) != null ? _ref : ''
|
|
269
|
+
}, doc.data());
|
|
270
|
+
});
|
|
271
|
+
const settled = commerceSettledSummary(rows, (swept == null ? void 0 : swept.truncated) === true);
|
|
272
|
+
const byHost = commerceHostAttribution(rows, request.attributionLimit);
|
|
273
|
+
await request.nameRows(byHost.rows, {
|
|
274
|
+
collection: 'hosts',
|
|
275
|
+
nameField: 'displayName',
|
|
276
|
+
detailField: 'subdomain'
|
|
277
|
+
});
|
|
278
|
+
return commerceRevenueAnswer({
|
|
279
|
+
settled,
|
|
280
|
+
byHost,
|
|
281
|
+
failed: swept === null
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
//# sourceMappingURL=revenue-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/revenue-source.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 * The storefront's take on the operator's revenue report (AGL-2486, AGL-3080).\n *\n * Answered through the platform's revenue source contract\n * (`@aglyn/aglyn/plugin-manager/plugin-revenue-sources`). The report folds the\n * operator's own invoices; this is what storefront orders earned it — and the\n * one figure on that page most likely to be reported wrongly.\n *\n * An order's `feeCents` is the Connect `application_fee_amount`, which since\n * AGL-2152 is the advertised take PLUS Stripe's processing cost:\n *\n * fee = take%(goods) + processing%(charge) + 30¢\n *\n * Every storefront charge is a DESTINATION charge, so Stripe moves the whole\n * amount to the merchant and debits its processing fee from the PLATFORM's\n * balance; the pass-through half of that fee exists purely to recover a cost\n * the platform has already paid. Reporting `feeCents` as earnings would\n * overstate the margin on every single storefront sale — and on a small order\n * it would report the 30¢ Stripe just took as money the platform made.\n *\n * So the pass-through is recomputed with the SAME helper the fee was charged\n * from (`storefrontProcessingCostCents`) and subtracted. Both halves are\n * reported, because \"we collected X of which Y was Stripe's\" is the sentence a\n * margin figure needs.\n *\n * The recomputation is an estimate in one direction only: it re-derives the\n * cost from the stored charge amount rather than reading Stripe's actual\n * balance-transaction fee, and it prices it at the DEAREST enabled payment\n * method's rate (BNPL, 6%) because that is the rate the fee was charged at.\n * A card-family order really costs 2.9%, so on those orders this subtracts\n * more than Stripe took and the take is UNDERSTATED — the safe direction, and\n * stated on the page rather than left to be discovered.\n *\n * ## Subscription cycles, and the one cycle this understates\n *\n * A storefront SUBSCRIPTION cycle carries a `subscriptionId`. Since AGL-2655\n * its fee carries the same pass-through, folded into\n * `application_fee_percent` as a rate because a Stripe subscription cannot\n * express a fixed 30¢ — so it is netted out here exactly as a one-time sale's\n * is. The percent is sized on the recurring goods and the recomputed cost here\n * is sized on the whole amount paid, tax included, so a taxed cycle subtracts\n * slightly more than was recovered: understated, the safe direction.\n *\n * A subscription sold before the pass-through existed is carried onto it by\n * the renewal re-price (AGL-2289) at its next paid invoice — but THAT invoice\n * was billed at the bare take, so its row's `feeCents` holds no pass-through\n * and the clamp reports its take as zero. One cycle per legacy subscription,\n * and the count beside the figure is how a reader sees how much of the book\n * that is.\n */\n\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { storefrontProcessingCostCents } from '@aglyn/aglyn/server'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { stripeIdIsTestMode } from '@aglyn/aglyn/app-utils/stripe-deployment-mode'\nimport {\n groupRevenueAttribution,\n type RevenueAttribution,\n type RevenueSource,\n type RevenueSourceAnswer,\n} from '@aglyn/aglyn/plugin-manager/plugin-revenue-sources'\n\n/** One `hosts/{hostId}/orders/{orderId}` row — a storefront sale. */\nexport interface CommerceOrderRowInput {\n id: string\n /** What the shopper paid, tax and shipping included. */\n amountCents?: unknown\n /**\n * The Connect `application_fee_amount`: the platform's advertised take PLUS\n * Stripe's processing cost passed through at cost (AGL-2152). NOT margin —\n * see the module note.\n */\n feeCents?: unknown\n /**\n * Present ONLY on a storefront SUBSCRIPTION renewal, and it changes how\n * `feeCents` must be read — see the module note.\n */\n subscriptionId?: unknown\n /** Merchant refunds AND chargebacks both land here (`refund.ts`). */\n refundedCents?: unknown\n /**\n * WHICH storefront the order belongs to — the attribution key.\n *\n * Orders live at `hosts/{hostId}/orders/{orderId}`, so this is lifted from\n * the document PATH by the reader rather than read from a field. It costs no\n * extra read and cannot disagree with where the document actually is.\n */\n hostId?: unknown\n createdAt?: unknown\n /**\n * Whether Stripe moved real money, stamped from `event.livemode` by the\n * webhook. Authoritative when present, and absent on every order written\n * before that stamp shipped — which is why {@link isTestModeOrderRow} falls\n * back to the id below rather than treating absence as an answer.\n */\n livemode?: unknown\n /**\n * The Checkout Session that produced the order. It carries its own mode\n * (`cs_test_…`), which a subscription renewal's invoice id does not — so\n * this is the fallback discriminator for rows predating `livemode`.\n */\n checkoutSessionId?: unknown\n}\n\nexport interface CommerceSettled {\n transactionCount: number\n /** Shopper spend across storefronts. Overwhelmingly the merchant's. */\n grossCents: number\n /** The whole `application_fee_amount` the platform collected. NOT margin. */\n applicationFeeCents: number\n /** Stripe's processing cost this recovered, at cost. Subtracted, never earnings. */\n processingPassThroughCents: number\n /** Application fee minus the pass-through: the advertised take. */\n commissionCents: number\n /** Take handed back with refunds and chargebacks, pro-rata. */\n commissionRefundedCents: number\n /** Take net of reversals. */\n commissionNetCents: number\n /**\n * Storefront SUBSCRIPTION cycles in this period. Counted because a cycle\n * billed before its subscription was re-priced onto the pass-through\n * reports no take at all — see the module note.\n */\n subscriptionOrders: number\n /**\n * The sweep hit its ceiling, so every figure above is a LOWER BOUND and must\n * not be quoted as a total. Narrow the period instead.\n */\n truncated: boolean\n}\n\n/** Cents from anything, defaulting to 0 rather than NaN. */\nfunction cents(value: unknown): number {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) ? Math.round(parsed) : 0\n}\n\n/**\n * Whether this order row was a test-mode rehearsal.\n *\n * A recorded `livemode` wins, and the session id's prefix is the fallback for\n * rows written before anything recorded it. An order with neither signal is\n * LIVE: a POS cash sale carries no Stripe session at all, and answering \"test\"\n * for anything unidentifiable would erase genuine revenue from the report.\n */\nfunction isTestModeOrderRow(row: CommerceOrderRowInput): boolean {\n if (typeof row.livemode === 'boolean') return !row.livemode\n return stripeIdIsTestMode(row.checkoutSessionId ?? row.id)\n}\n\nexport function commerceSettledSummary(\n rows: readonly CommerceOrderRowInput[],\n truncated = false,\n): CommerceSettled {\n const out: CommerceSettled = {\n transactionCount: 0,\n grossCents: 0,\n applicationFeeCents: 0,\n processingPassThroughCents: 0,\n commissionCents: 0,\n commissionRefundedCents: 0,\n commissionNetCents: 0,\n subscriptionOrders: 0,\n truncated: truncated === true,\n }\n for (const row of rows ?? []) {\n // A REHEARSAL IS NOT REVENUE. A smoke-test checkout writes a real order\n // document — Stripe never moved money for it, and its session id says so.\n // Skipped ENTIRELY rather than counted at zero, because `transactionCount`\n // is read as \"how many sales\", and a rehearsal is not one.\n if (isTestModeOrderRow(row)) continue\n const split = commerceOrderTake(row)\n out.transactionCount += 1\n out.grossCents += split.gross\n out.applicationFeeCents += split.fee\n if (split.isSubscriptionOrder) out.subscriptionOrders += 1\n out.processingPassThroughCents += split.passThrough\n out.commissionCents += split.take\n out.commissionRefundedCents += split.takeRefunded\n out.commissionNetCents += split.takeNet\n }\n return out\n}\n\n/**\n * One storefront order's take, in ONE place.\n *\n * Shared with the per-host attribution so a host table cannot sum to\n * something other than the storefront line above it.\n *\n * A zero-fee order contributes its gross and its count and nothing else —\n * there is no take to attribute, and inventing one from the gross would\n * report a merchant's money as the platform's.\n */\nexport function commerceOrderTake(row: CommerceOrderRowInput | null): {\n gross: number\n fee: number\n isSubscriptionOrder: boolean\n passThrough: number\n take: number\n takeRefunded: number\n takeNet: number\n} {\n const gross = cents(row?.amountCents)\n const fee = Math.max(0, cents(row?.feeCents))\n const isSubscriptionOrder =\n typeof row?.subscriptionId === 'string' && row.subscriptionId.length > 0\n if (fee <= 0) {\n return {\n gross,\n fee,\n isSubscriptionOrder,\n passThrough: 0,\n take: 0,\n takeRefunded: 0,\n takeNet: 0,\n }\n }\n // Every fee bundles the recovery — a one-time sale's in cents, a\n // subscription cycle's as a rate (AGL-2655). Clamped to the fee itself: the\n // recomputed cost can exceed a fee charged under an older rate, and a\n // negative take would subtract from another order's real margin.\n const passThrough = Math.min(fee, storefrontProcessingCostCents(gross))\n const take = fee - passThrough\n const refunded = Math.min(gross, Math.max(0, cents(row?.refundedCents)))\n const takeRefunded = gross > 0 ? Math.round((refunded * take) / gross) : 0\n return {\n gross,\n fee,\n isSubscriptionOrder,\n passThrough,\n take,\n takeRefunded,\n takeNet: take - takeRefunded,\n }\n}\n\n/**\n * Storefront take by HOST.\n *\n * By host and not by org on purpose: one org can run several storefronts, and\n * \"which store earns\" is the question a per-org roll-up destroys.\n */\nexport function commerceHostAttribution(\n rows: readonly CommerceOrderRowInput[],\n limit = 100,\n): RevenueAttribution {\n return groupRevenueAttribution(\n (rows ?? []).map((row) => {\n const split = commerceOrderTake(row)\n return {\n key: String(row?.hostId ?? ''),\n detail: '',\n gain: split.takeNet,\n loss: split.takeRefunded,\n }\n }),\n limit,\n 'Host not recorded',\n )\n}\n\n/**\n * The storefront's answer for one period, from figures already folded. Pure,\n * so what the report prints can be proved from fixtures.\n *\n * `failed` is a sweep that could not run — a collection-group index this\n * deployment has not built answers FAILED_PRECONDITION — and it is reported as\n * a failure with figures of zero, never as a cap: \"we read part of it\" and \"we\n * read none of it\" have different remedies.\n */\nexport function commerceRevenueAnswer(read: {\n settled: CommerceSettled\n byHost: RevenueAttribution\n failed: boolean\n}): RevenueSourceAnswer {\n const brand = PLATFORM_BRAND_NAME\n const { settled } = read\n return {\n id: 'commerce',\n name: 'storefront orders',\n earned: {\n label: 'Storefront commission',\n cents: Number(settled.commissionNetCents ?? 0),\n note:\n 'The advertised take on merchant storefront sales, net of refunds — ' +\n 'with Stripe’s card processing removed. The platform fee charged on a ' +\n 'storefront sale bundles that processing cost and passes it through at ' +\n 'cost; it is a recovery, not earnings.',\n },\n grossToNet: [\n {\n label: 'Storefront sales (shopper gross)',\n cents: settled.grossCents,\n deduction: false,\n note:\n 'The merchant’s. It transfers straight to their connected account ' +\n 'and is shown only for scale.',\n },\n {\n label: 'Storefront platform fee collected',\n cents: settled.applicationFeeCents,\n deduction: false,\n note: `Not all ${brand}’s — see the next line.`,\n },\n {\n label: '— less card processing passed through at cost',\n cents: settled.processingPassThroughCents,\n deduction: true,\n note: `Stripe’s. On a destination charge Stripe debits ${brand}’s balance for processing, and this half of the fee recovers exactly that. It is a recovery, not earnings, and reporting it as revenue would overstate every storefront sale.`,\n },\n ],\n notes:\n Number(settled.subscriptionOrders ?? 0) > 0\n ? [\n {\n label: `${settled.subscriptionOrders} storefront subscription cycles — one billed before its re-price reports no take`,\n tone: 'warning',\n },\n ]\n : [],\n attribution: [\n {\n id: 'byHost',\n heading: 'Storefront take by host',\n unit: 'Storefront',\n countLabel: 'Orders',\n empty: `No storefront order settled in this period. Note this is ${brand}'s take only — the shopper's spend is the merchant's money and is never counted here.`,\n chartEmpty: 'No storefront earned a take in this period — nothing to plot yet.',\n ...read.byHost,\n },\n ],\n attributionNote:\n \"Storefront figures above are the advertised take with Stripe's \" +\n 'processing cost already subtracted, on the basis stated in “Gross ' +\n 'versus net” below. Attribution is by host rather than by org because ' +\n 'one org can run several storefronts, and rolling them up destroys the ' +\n 'question.',\n truncated: settled.truncated,\n failure: read.failed\n ? {\n title: 'Storefront orders could not be read',\n detail:\n 'The storefront commission below reads $0 because the query ' +\n 'failed, not because there were no sales. The sweep needs the ' +\n 'COLLECTION_GROUP index on orders.createdAtMs — check it is still ' +\n 'declared in the Firestore index config and actually deployed, ' +\n 'since indexes ship separately from the app.',\n }\n : null,\n summary: settled,\n }\n}\n\n/**\n * The storefront's source, registered on the console's API surface: the\n * staff revenue report awaits that surface before it asks.\n */\nexport const commerceRevenueSource: RevenueSource = {\n async read(request) {\n const firestore = firebaseAdmin.app().firestore()\n // Storefront orders live per host (`hosts/{hostId}/orders`), so this is a\n // collection-GROUP range — ranged on `createdAtMs`, the NUMBER, and not on\n // the `createdAt` Timestamp beside it. A collection-group range needs a\n // COLLECTION_GROUP-scoped single-field index — Firestore's automatic ones\n // are COLLECTION scope only — and the index that is declared and deployed\n // is on `createdAtMs` (AGL-1793). The `createdAt` form answers\n // FAILED_PRECONDITION on every request, which is how the report once read\n // $0 for every storefront.\n let swept: {\n docs: FirebaseFirestore.QueryDocumentSnapshot[]\n truncated: boolean\n } | null\n try {\n swept = await request.sweep<FirebaseFirestore.QueryDocumentSnapshot>(\n firestore\n .collectionGroup('orders')\n .where('createdAtMs', '>=', request.start.getTime())\n .where('createdAtMs', '<', request.end.getTime()),\n 'createdAtMs',\n )\n } catch {\n swept = null\n }\n const rows: CommerceOrderRowInput[] = (swept?.docs ?? []).map((doc) => ({\n id: doc.id,\n // The storefront that earned the take is in the PATH.\n hostId: doc.ref.parent.parent?.id ?? '',\n ...(doc.data() as object),\n }))\n const settled = commerceSettledSummary(rows, swept?.truncated === true)\n const byHost = commerceHostAttribution(rows, request.attributionLimit)\n await request.nameRows(byHost.rows, {\n collection: 'hosts',\n nameField: 'displayName',\n detailField: 'subdomain',\n })\n return commerceRevenueAnswer({ settled, byHost, failed: swept === null })\n },\n}\n"],"names":["firebaseAdmin","storefrontProcessingCostCents","PLATFORM_BRAND_NAME","stripeIdIsTestMode","groupRevenueAttribution","cents","value","parsed","Number","isFinite","Math","round","isTestModeOrderRow","row","livemode","checkoutSessionId","id","commerceSettledSummary","rows","truncated","out","transactionCount","grossCents","applicationFeeCents","processingPassThroughCents","commissionCents","commissionRefundedCents","commissionNetCents","subscriptionOrders","split","commerceOrderTake","gross","fee","isSubscriptionOrder","passThrough","take","takeRefunded","takeNet","amountCents","max","feeCents","subscriptionId","length","min","refunded","refundedCents","commerceHostAttribution","limit","map","key","String","hostId","detail","gain","loss","commerceRevenueAnswer","read","settled","brand","name","earned","label","note","grossToNet","deduction","notes","tone","attribution","heading","unit","countLabel","empty","chartEmpty","byHost","attributionNote","failure","failed","title","summary","commerceRevenueSource","request","firestore","app","swept","sweep","collectionGroup","where","start","getTime","end","docs","doc","ref","parent","data","attributionLimit","nameRows","collection","nameField","detailField"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDC,GAED,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SAASC,6BAA6B,QAAQ,sBAAqB;AACnE,SAASC,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,kBAAkB,QAAQ,gDAA+C;AAClF,SACEC,uBAAuB,QAIlB,qDAAoD;AAuE3D,0DAA0D,GAC1D,SAASC,MAAMC,KAAc;IAC3B,MAAMC,SAASC,OAAOF,gBAAAA,QAAS;IAC/B,OAAOE,OAAOC,QAAQ,CAACF,UAAUG,KAAKC,KAAK,CAACJ,UAAU;AACxD;AAEA;;;;;;;CAOC,GACD,SAASK,mBAAmBC,GAA0B;QAE1BA;IAD1B,IAAI,OAAOA,IAAIC,QAAQ,KAAK,WAAW,OAAO,CAACD,IAAIC,QAAQ;IAC3D,OAAOX,oBAAmBU,yBAAAA,IAAIE,iBAAiB,YAArBF,yBAAyBA,IAAIG,EAAE;AAC3D;AAEA,OAAO,SAASC,uBACdC,IAAsC,EACtCC,YAAY,KAAK;IAEjB,MAAMC,MAAuB;QAC3BC,kBAAkB;QAClBC,YAAY;QACZC,qBAAqB;QACrBC,4BAA4B;QAC5BC,iBAAiB;QACjBC,yBAAyB;QACzBC,oBAAoB;QACpBC,oBAAoB;QACpBT,WAAWA,cAAc;IAC3B;IACA,KAAK,MAAMN,OAAOK,eAAAA,OAAQ,EAAE,CAAE;QAC5B,wEAAwE;QACxE,0EAA0E;QAC1E,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAIN,mBAAmBC,MAAM;QAC7B,MAAMgB,QAAQC,kBAAkBjB;QAChCO,IAAIC,gBAAgB,IAAI;QACxBD,IAAIE,UAAU,IAAIO,MAAME,KAAK;QAC7BX,IAAIG,mBAAmB,IAAIM,MAAMG,GAAG;QACpC,IAAIH,MAAMI,mBAAmB,EAAEb,IAAIQ,kBAAkB,IAAI;QACzDR,IAAII,0BAA0B,IAAIK,MAAMK,WAAW;QACnDd,IAAIK,eAAe,IAAII,MAAMM,IAAI;QACjCf,IAAIM,uBAAuB,IAAIG,MAAMO,YAAY;QACjDhB,IAAIO,kBAAkB,IAAIE,MAAMQ,OAAO;IACzC;IACA,OAAOjB;AACT;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASU,kBAAkBjB,GAAiC;IASjE,MAAMkB,QAAQ1B,MAAMQ,uBAAAA,IAAKyB,WAAW;IACpC,MAAMN,MAAMtB,KAAK6B,GAAG,CAAC,GAAGlC,MAAMQ,uBAAAA,IAAK2B,QAAQ;IAC3C,MAAMP,sBACJ,QAAOpB,uBAAAA,IAAK4B,cAAc,MAAK,YAAY5B,IAAI4B,cAAc,CAACC,MAAM,GAAG;IACzE,IAAIV,OAAO,GAAG;QACZ,OAAO;YACLD;YACAC;YACAC;YACAC,aAAa;YACbC,MAAM;YACNC,cAAc;YACdC,SAAS;QACX;IACF;IACA,iEAAiE;IACjE,4EAA4E;IAC5E,sEAAsE;IACtE,iEAAiE;IACjE,MAAMH,cAAcxB,KAAKiC,GAAG,CAACX,KAAK/B,8BAA8B8B;IAChE,MAAMI,OAAOH,MAAME;IACnB,MAAMU,WAAWlC,KAAKiC,GAAG,CAACZ,OAAOrB,KAAK6B,GAAG,CAAC,GAAGlC,MAAMQ,uBAAAA,IAAKgC,aAAa;IACrE,MAAMT,eAAeL,QAAQ,IAAIrB,KAAKC,KAAK,CAAC,AAACiC,WAAWT,OAAQJ,SAAS;IACzE,OAAO;QACLA;QACAC;QACAC;QACAC;QACAC;QACAC;QACAC,SAASF,OAAOC;IAClB;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASU,wBACd5B,IAAsC,EACtC6B,QAAQ,GAAG;IAEX,OAAO3C,wBACL,CAACc,eAAAA,OAAQ,EAAE,EAAE8B,GAAG,CAAC,CAACnC;;QAChB,MAAMgB,QAAQC,kBAAkBjB;QAChC,OAAO;YACLoC,KAAKC,eAAOrC,uBAAAA,IAAKsC,MAAM,mBAAI;YAC3BC,QAAQ;YACRC,MAAMxB,MAAMQ,OAAO;YACnBiB,MAAMzB,MAAMO,YAAY;QAC1B;IACF,IACAW,OACA;AAEJ;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASQ,sBAAsBC,IAIrC;QAQmBC,6BA8BPA;IArCX,MAAMC,QAAQxD;IACd,MAAM,EAAEuD,OAAO,EAAE,GAAGD;IACpB,OAAO;QACLxC,IAAI;QACJ2C,MAAM;QACNC,QAAQ;YACNC,OAAO;YACPxD,OAAOG,QAAOiD,8BAAAA,QAAQ9B,kBAAkB,YAA1B8B,8BAA8B;YAC5CK,MACE,wEACA,0EACA,2EACA;QACJ;QACAC,YAAY;YACV;gBACEF,OAAO;gBACPxD,OAAOoD,QAAQnC,UAAU;gBACzB0C,WAAW;gBACXF,MACE,sEACA;YACJ;YACA;gBACED,OAAO;gBACPxD,OAAOoD,QAAQlC,mBAAmB;gBAClCyC,WAAW;gBACXF,MAAM,CAAC,QAAQ,EAAEJ,MAAM,uBAAuB,CAAC;YACjD;YACA;gBACEG,OAAO;gBACPxD,OAAOoD,QAAQjC,0BAA0B;gBACzCwC,WAAW;gBACXF,MAAM,CAAC,gDAAgD,EAAEJ,MAAM,6KAA6K,CAAC;YAC/O;SACD;QACDO,OACEzD,QAAOiD,8BAAAA,QAAQ7B,kBAAkB,YAA1B6B,8BAA8B,KAAK,IACtC;YACE;gBACEI,OAAO,GAAGJ,QAAQ7B,kBAAkB,CAAC,gFAAgF,CAAC;gBACtHsC,MAAM;YACR;SACD,GACD,EAAE;QACRC,aAAa;YACX;gBACEnD,IAAI;gBACJoD,SAAS;gBACTC,MAAM;gBACNC,YAAY;gBACZC,OAAO,CAAC,yDAAyD,EAAEb,MAAM,qFAAqF,CAAC;gBAC/Jc,YAAY;eACThB,KAAKiB,MAAM;SAEjB;QACDC,iBACE,oEACA,uEACA,0EACA,2EACA;QACFvD,WAAWsC,QAAQtC,SAAS;QAC5BwD,SAASnB,KAAKoB,MAAM,GAChB;YACEC,OAAO;YACPzB,QACE,gEACA,kEACA,sEACA,mEACA;QACJ,IACA;QACJ0B,SAASrB;IACX;AACF;AAEA;;;CAGC,GACD,OAAO,MAAMsB,wBAAuC;IAClD,MAAMvB,MAAKwB,OAAO;;QAChB,MAAMC,YAAYjF,cAAckF,GAAG,GAAGD,SAAS;QAC/C,0EAA0E;QAC1E,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,0EAA0E;QAC1E,+DAA+D;QAC/D,0EAA0E;QAC1E,2BAA2B;QAC3B,IAAIE;QAIJ,IAAI;YACFA,QAAQ,MAAMH,QAAQI,KAAK,CACzBH,UACGI,eAAe,CAAC,UAChBC,KAAK,CAAC,eAAe,MAAMN,QAAQO,KAAK,CAACC,OAAO,IAChDF,KAAK,CAAC,eAAe,KAAKN,QAAQS,GAAG,CAACD,OAAO,KAChD;QAEJ,EAAE,eAAM;YACNL,QAAQ;QACV;QACA,MAAMjE,OAAgC,SAACiE,yBAAAA,MAAOO,IAAI,mBAAI,EAAE,EAAE1C,GAAG,CAAC,CAAC2C;;gBAGrDA;mBAH8D;gBACtE3E,IAAI2E,IAAI3E,EAAE;gBACV,sDAAsD;gBACtDmC,MAAM,WAAEwC,yBAAAA,IAAIC,GAAG,CAACC,MAAM,CAACA,MAAM,qBAArBF,uBAAuB3E,EAAE,mBAAI;eACjC2E,IAAIG,IAAI;;QAEd,MAAMrC,UAAUxC,uBAAuBC,MAAMiE,CAAAA,yBAAAA,MAAOhE,SAAS,MAAK;QAClE,MAAMsD,SAAS3B,wBAAwB5B,MAAM8D,QAAQe,gBAAgB;QACrE,MAAMf,QAAQgB,QAAQ,CAACvB,OAAOvD,IAAI,EAAE;YAClC+E,YAAY;YACZC,WAAW;YACXC,aAAa;QACf;QACA,OAAO5C,sBAAsB;YAAEE;YAASgB;YAAQG,QAAQO,UAAU;QAAK;IACzE;AACF,EAAC"}
|
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* The merchant's tax rule, published for every plugin that charges.
|
|
19
19
|
*
|
|
20
|
-
* A merchant sets tax once, in the store settings this plugin keeps
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* A merchant sets tax once, in the store settings this plugin keeps
|
|
21
|
+
* (`hosts/{hostId}/settings/store`, under `tax`). A plugin that sells
|
|
22
|
+
* something else — an appointment — prices its own charge by asking the
|
|
23
|
+
* tax-profile contract: for the site's rate for that kind of charge, read
|
|
24
|
+
* here, and for these same two functions over it — one rounding rule, one
|
|
25
|
+
* reading of a settled payment, wherever money is taken.
|
|
25
26
|
*
|
|
26
27
|
* Registered from BOTH server registrars, because a booking is priced in the
|
|
27
28
|
* tenant app and confirmed by the console's billing webhook.
|
|
28
29
|
*/
|
|
29
30
|
export declare function registerCommerceTaxProfile(): void;
|
|
31
|
+
/** One site's stored flat rate for a kind of charge, as stored. */
|
|
32
|
+
export declare function readFlatRate(hostId: string, charge: string): Promise<unknown>;
|
|
@@ -14,27 +14,44 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { registerPluginTaxProfile } from "@aglyn/aglyn/plugin-manager/plugin-tax-profile";
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
17
18
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
18
19
|
import { resolveFlatTaxCents } from "../model/index.js";
|
|
19
20
|
import { storefrontTaxModeOf } from "./storefront-tax.js";
|
|
20
21
|
/**
|
|
21
22
|
* The merchant's tax rule, published for every plugin that charges.
|
|
22
23
|
*
|
|
23
|
-
* A merchant sets tax once, in the store settings this plugin keeps
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* A merchant sets tax once, in the store settings this plugin keeps
|
|
25
|
+
* (`hosts/{hostId}/settings/store`, under `tax`). A plugin that sells
|
|
26
|
+
* something else — an appointment — prices its own charge by asking the
|
|
27
|
+
* tax-profile contract: for the site's rate for that kind of charge, read
|
|
28
|
+
* here, and for these same two functions over it — one rounding rule, one
|
|
29
|
+
* reading of a settled payment, wherever money is taken.
|
|
28
30
|
*
|
|
29
31
|
* Registered from BOTH server registrars, because a booking is priced in the
|
|
30
32
|
* tenant app and confirmed by the console's billing webhook.
|
|
31
33
|
*/ export function registerCommerceTaxProfile() {
|
|
32
34
|
registerPluginTaxProfile({
|
|
35
|
+
flatRate: (hostId, charge)=>readFlatRate(hostId, charge),
|
|
33
36
|
flatTax: (rate, chargeCents, fallbackLabel)=>resolveFlatTaxCents(rate, chargeCents, fallbackLabel),
|
|
34
37
|
taxModeOf: (settledPayment, manualTaxCents)=>storefrontTaxModeOf(settledPayment, manualTaxCents)
|
|
35
38
|
}, {
|
|
36
39
|
pluginId: BUNDLE_ID
|
|
37
40
|
});
|
|
38
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The flat rates the store settings keep, by the kind of charge a caller
|
|
44
|
+
* names. A kind not listed has no rate here, and answers `undefined`.
|
|
45
|
+
*/ const FLAT_RATE_CHARGES = {
|
|
46
|
+
service: 'service',
|
|
47
|
+
lodging: 'lodging'
|
|
48
|
+
};
|
|
49
|
+
/** One site's stored flat rate for a kind of charge, as stored. */ export async function readFlatRate(hostId, charge) {
|
|
50
|
+
const field = FLAT_RATE_CHARGES[charge];
|
|
51
|
+
if (!hostId || !field) return undefined;
|
|
52
|
+
const snapshot = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('settings').doc('store').get();
|
|
53
|
+
const tax = snapshot.exists ? snapshot.get('tax') : undefined;
|
|
54
|
+
return tax == null ? void 0 : tax[field];
|
|
55
|
+
}
|
|
39
56
|
|
|
40
57
|
//# sourceMappingURL=tax-profile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/tax-profile.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 { registerPluginTaxProfile } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { type FlatTaxRate, resolveFlatTaxCents } from '../model'\nimport { storefrontTaxModeOf } from './storefront-tax'\n\n/**\n * The merchant's tax rule, published for every plugin that charges.\n *\n * A merchant sets tax once, in the store settings this plugin keeps. A plugin\n *
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/tax-profile.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 { registerPluginTaxProfile } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { type FlatTaxRate, resolveFlatTaxCents, type TaxSettings } from '../model'\nimport { storefrontTaxModeOf } from './storefront-tax'\n\n/**\n * The merchant's tax rule, published for every plugin that charges.\n *\n * A merchant sets tax once, in the store settings this plugin keeps\n * (`hosts/{hostId}/settings/store`, under `tax`). A plugin that sells\n * something else — an appointment — prices its own charge by asking the\n * tax-profile contract: for the site's rate for that kind of charge, read\n * here, and for these same two functions over it — one rounding rule, one\n * reading of a settled payment, wherever money is taken.\n *\n * Registered from BOTH server registrars, because a booking is priced in the\n * tenant app and confirmed by the console's billing webhook.\n */\nexport function registerCommerceTaxProfile(): void {\n registerPluginTaxProfile(\n {\n flatRate: (hostId, charge) => readFlatRate(hostId, charge),\n flatTax: (rate, chargeCents, fallbackLabel) =>\n resolveFlatTaxCents(\n rate as FlatTaxRate | undefined | null,\n chargeCents,\n fallbackLabel,\n ),\n taxModeOf: (settledPayment, manualTaxCents) =>\n storefrontTaxModeOf(settledPayment, manualTaxCents),\n },\n { pluginId: BUNDLE_ID },\n )\n}\n\n/**\n * The flat rates the store settings keep, by the kind of charge a caller\n * names. A kind not listed has no rate here, and answers `undefined`.\n */\nconst FLAT_RATE_CHARGES: Readonly<Record<string, keyof TaxSettings>> = {\n service: 'service',\n lodging: 'lodging',\n}\n\n/** One site's stored flat rate for a kind of charge, as stored. */\nexport async function readFlatRate(\n hostId: string,\n charge: string,\n): Promise<unknown> {\n const field = FLAT_RATE_CHARGES[charge]\n if (!hostId || !field) return undefined\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('settings')\n .doc('store')\n .get()\n const tax = (snapshot.exists ? snapshot.get('tax') : undefined) as\n | TaxSettings\n | undefined\n return tax?.[field]\n}\n"],"names":["registerPluginTaxProfile","firebaseAdmin","BUNDLE_ID","resolveFlatTaxCents","storefrontTaxModeOf","registerCommerceTaxProfile","flatRate","hostId","charge","readFlatRate","flatTax","rate","chargeCents","fallbackLabel","taxModeOf","settledPayment","manualTaxCents","pluginId","FLAT_RATE_CHARGES","service","lodging","field","undefined","snapshot","app","firestore","collection","doc","get","tax","exists"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,QAAQ,iDAAgD;AACzF,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAA2BC,mBAAmB,QAA0B,oBAAU;AAClF,SAASC,mBAAmB,QAAQ,sBAAkB;AAEtD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC;IACdL,yBACE;QACEM,UAAU,CAACC,QAAQC,SAAWC,aAAaF,QAAQC;QACnDE,SAAS,CAACC,MAAMC,aAAaC,gBAC3BV,oBACEQ,MACAC,aACAC;QAEJC,WAAW,CAACC,gBAAgBC,iBAC1BZ,oBAAoBW,gBAAgBC;IACxC,GACA;QAAEC,UAAUf;IAAU;AAE1B;AAEA;;;CAGC,GACD,MAAMgB,oBAAiE;IACrEC,SAAS;IACTC,SAAS;AACX;AAEA,iEAAiE,GACjE,OAAO,eAAeX,aACpBF,MAAc,EACdC,MAAc;IAEd,MAAMa,QAAQH,iBAAiB,CAACV,OAAO;IACvC,IAAI,CAACD,UAAU,CAACc,OAAO,OAAOC;IAC9B,MAAMC,WAAW,MAAMtB,cACpBuB,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACpB,QACJmB,UAAU,CAAC,YACXC,GAAG,CAAC,SACJC,GAAG;IACN,MAAMC,MAAON,SAASO,MAAM,GAAGP,SAASK,GAAG,CAAC,SAASN;IAGrD,OAAOO,uBAAAA,GAAK,CAACR,MAAM;AACrB"}
|
|
@@ -0,0 +1,125 @@
|
|
|
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 { TaxReturnFiling, TaxReturnFilingLine, TaxReturnFinding, TaxReturnSource, TaxReturnSourceAnswer, TaxReturnSourceRequest } from '@aglyn/aglyn/plugin-manager/plugin-tax-return-sources';
|
|
18
|
+
import { type StorefrontTaxReturnRowInput, type StorefrontTaxSummary } from '../model/storefront-tax-summary';
|
|
19
|
+
/** One row of the storefront listing, as the working papers carry it. */
|
|
20
|
+
export interface StorefrontTaxRow {
|
|
21
|
+
id: string;
|
|
22
|
+
hostId: string | null;
|
|
23
|
+
orgId: string | null;
|
|
24
|
+
paidAt: string | null;
|
|
25
|
+
taxMode: string | null;
|
|
26
|
+
taxLiability: string | null;
|
|
27
|
+
grossCents: number;
|
|
28
|
+
taxCents: number;
|
|
29
|
+
taxableSalesCents: number;
|
|
30
|
+
state: string | null;
|
|
31
|
+
country: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** A liability bucket, ready to print. */
|
|
34
|
+
export interface StorefrontTaxBucketRow {
|
|
35
|
+
id: 'aglynLiable' | 'merchantManual' | 'connectedAccountLiable';
|
|
36
|
+
label: string;
|
|
37
|
+
/** Who owes it — the sentence that decides whether it is on this return. */
|
|
38
|
+
liability: string;
|
|
39
|
+
transactionCount: number;
|
|
40
|
+
grossDollars: string;
|
|
41
|
+
taxableSalesDollars: string;
|
|
42
|
+
taxCollectedDollars: string;
|
|
43
|
+
/** True when this row is money in the platform's balance under its registration. */
|
|
44
|
+
platformLiable: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** One state's facilitated storefront sales, for the nexus question. */
|
|
47
|
+
export interface StorefrontFacilitatedJurisdictionRow {
|
|
48
|
+
jurisdiction: string;
|
|
49
|
+
/** True for the one jurisdiction this deployment files a return in. */
|
|
50
|
+
isFilingJurisdiction: boolean;
|
|
51
|
+
transactionCount: number;
|
|
52
|
+
totalSalesDollars: string;
|
|
53
|
+
taxCollectedDollars: string;
|
|
54
|
+
/** The part of `taxCollectedDollars` the platform holds and must remit. */
|
|
55
|
+
platformLiableTaxDollars: string;
|
|
56
|
+
/** True when NO tax was collected on any sale into this state. */
|
|
57
|
+
untaxed: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Storefront tax in the FILING jurisdiction that Stripe computed against the
|
|
61
|
+
* platform's own registrations.
|
|
62
|
+
*
|
|
63
|
+
* The one figure that decides whether a period can be filed from the filing
|
|
64
|
+
* lines alone. Deliberately excludes `merchantManual` — a merchant's own
|
|
65
|
+
* configured rate never touched those registrations. Read against the
|
|
66
|
+
* configured jurisdiction rather than Texas, because a hard-coded key answers
|
|
67
|
+
* `0.00` everywhere else — and a zero here reads as "nothing to decide" on
|
|
68
|
+
* the one finding that blocks filing.
|
|
69
|
+
*/
|
|
70
|
+
export declare function storefrontPlatformLiableCents(summary: StorefrontTaxSummary | null | undefined, filing: TaxReturnFiling): number;
|
|
71
|
+
/** The storefront's findings. The platform raises the non-zero ones. */
|
|
72
|
+
export declare function storefrontTaxFindings(summary: StorefrontTaxSummary, filing: TaxReturnFiling): TaxReturnFinding[];
|
|
73
|
+
/**
|
|
74
|
+
* The two lines beneath the filing figures: the platform-liable tax and the
|
|
75
|
+
* merchant's own rate, separately, and outside every form item.
|
|
76
|
+
*/
|
|
77
|
+
export declare function storefrontFilingLines(summary: StorefrontTaxSummary, filing: TaxReturnFiling): TaxReturnFilingLine[];
|
|
78
|
+
/** The three liability buckets, in the order a reader decides them. */
|
|
79
|
+
export declare function storefrontBucketRows(summary: StorefrontTaxSummary): StorefrontTaxBucketRow[];
|
|
80
|
+
/**
|
|
81
|
+
* FACILITATED SALES BY STATE — the economic-nexus question (AGL-1956).
|
|
82
|
+
*
|
|
83
|
+
* The question a state asks a marketplace facilitator is "how much did you
|
|
84
|
+
* facilitate INTO this state, and in how many transactions" — not "how much
|
|
85
|
+
* tax did you collect there". A state the operator is not registered in
|
|
86
|
+
* collects nothing by definition, which is exactly why collection cannot be
|
|
87
|
+
* the measure: the states worth watching are the ones showing $0 tax and a
|
|
88
|
+
* rising sales figure.
|
|
89
|
+
*
|
|
90
|
+
* So this SUMS the three liability buckets per jurisdiction. The rule that
|
|
91
|
+
* the operator's own invoices and storefront rows are never summed still
|
|
92
|
+
* holds — nothing here touches the operator's own sales; within the
|
|
93
|
+
* storefront collection the buckets differ only in WHO REMITS, and a nexus
|
|
94
|
+
* threshold counts the sale whoever remits it. Who remits is still carried
|
|
95
|
+
* per row, because "do we have nexus here" and "what do we owe here" are
|
|
96
|
+
* answered off the same rows and must not blur.
|
|
97
|
+
*
|
|
98
|
+
* ⚠️ A LOWER BOUND, deliberately rather than silently: `storefront-tax-record.ts`
|
|
99
|
+
* files no row at all for a sale whose `taxMode` resolves to `none`, so a
|
|
100
|
+
* wholly untaxed storefront sale is invisible here — the population nexus
|
|
101
|
+
* detection most needs. Closing it is a write-side change recorded on
|
|
102
|
+
* AGL-1956. The filing jurisdiction does not depend on any of this: a filer
|
|
103
|
+
* registered where it is established has no in-state threshold left to cross.
|
|
104
|
+
*/
|
|
105
|
+
export declare function storefrontFacilitatedJurisdictionRows(summary: StorefrontTaxSummary | null | undefined, filing: TaxReturnFiling): StorefrontFacilitatedJurisdictionRow[];
|
|
106
|
+
/**
|
|
107
|
+
* The storefront's answer for one period, from rows already read. Pure, so
|
|
108
|
+
* the section a preparer reads can be proved from fixtures.
|
|
109
|
+
*/
|
|
110
|
+
export declare function storefrontTaxReturnAnswer(read: {
|
|
111
|
+
rows: readonly StorefrontTaxReturnRowInput[];
|
|
112
|
+
truncated: boolean;
|
|
113
|
+
undatedRows: number;
|
|
114
|
+
}, request: TaxReturnSourceRequest): TaxReturnSourceAnswer;
|
|
115
|
+
/**
|
|
116
|
+
* The storefront's source, registered on the console's API surface: the
|
|
117
|
+
* staff return awaits that surface before it asks.
|
|
118
|
+
*
|
|
119
|
+
* Two queries, and the second is the honesty check: the period query ranges
|
|
120
|
+
* on `paidAt`, and a Firestore range query CANNOT match a document whose
|
|
121
|
+
* field is null — so a row that failed to date itself would be invisible to
|
|
122
|
+
* every period. The equality probe counts those rows, over all of them, and
|
|
123
|
+
* the platform blocks the return on any.
|
|
124
|
+
*/
|
|
125
|
+
export declare const commerceTaxReturnSource: TaxReturnSource;
|