@drawbridge/drawbridge-utils 0.0.151 → 0.0.152
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/dist/connections/index.cjs +82 -24
- package/dist/connections/index.d.cts +142 -45
- package/dist/connections/index.d.ts +142 -45
- package/dist/connections/index.js +82 -24
- package/dist/providers.cjs +82 -24
- package/dist/providers.js +82 -24
- package/package.json +1 -1
|
@@ -4147,7 +4147,7 @@ var shopify_default2 = {
|
|
|
4147
4147
|
// against the ones the app now needs, and queues a webhook
|
|
4148
4148
|
// reconciliation.
|
|
4149
4149
|
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
4150
|
-
var _a, _b, _c, _d;
|
|
4150
|
+
var _a, _b, _c, _d, _e, _f;
|
|
4151
4151
|
const request2 = {
|
|
4152
4152
|
connectionId: workflow.connection,
|
|
4153
4153
|
organizationId: workflow.organization,
|
|
@@ -4172,6 +4172,7 @@ var shopify_default2 = {
|
|
|
4172
4172
|
let metered;
|
|
4173
4173
|
let shopCurrency = null;
|
|
4174
4174
|
let billingProbeFailed = false;
|
|
4175
|
+
let reconciliation = null;
|
|
4175
4176
|
try {
|
|
4176
4177
|
if (!(source == null ? void 0 : source.id)) {
|
|
4177
4178
|
const shopData = await shopify.oauth.getShop({ adminAccessToken, shop: connection2.shop });
|
|
@@ -4192,45 +4193,99 @@ var shopify_default2 = {
|
|
|
4192
4193
|
const partnerRow = (read == null ? void 0 : read.get) ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
4193
4194
|
const partner = (partnerRow == null ? void 0 : partnerRow.settings) ? decrypt(partnerRow.settings) : {};
|
|
4194
4195
|
if (partner.partnerToken && partner.partnerOrgId && partner.partnerAppId && (source == null ? void 0 : source.id) && ((_b = shopify.partner) == null ? void 0 : _b.getUsageChargeEvents)) {
|
|
4196
|
+
const ledgerFloor = new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
4195
4197
|
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
4196
4198
|
appId: "gid://shopify/App/" + partner.partnerAppId,
|
|
4197
|
-
first:
|
|
4198
|
-
occurredAtMin:
|
|
4199
|
+
first: 10,
|
|
4200
|
+
occurredAtMin: ledgerFloor.toISOString(),
|
|
4199
4201
|
organizationId: partner.partnerOrgId,
|
|
4200
4202
|
partnerToken: partner.partnerToken,
|
|
4201
4203
|
shopId: "gid://shopify/Shop/" + source.id
|
|
4202
4204
|
});
|
|
4203
|
-
const
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4205
|
+
const boundaries = accruals.filter((event) => event.occurredAt).sort((a, b) => new Date(a.occurredAt) - new Date(b.occurredAt));
|
|
4206
|
+
const accrued = [...boundaries].reverse().find((event) => Number(event.usageQuantity) > 0);
|
|
4207
|
+
if (accrued) metered = String(accrued.chargeId || accrued.id);
|
|
4208
|
+
const stampedAt = (_c = boundaries.find(
|
|
4209
|
+
(event) => {
|
|
4210
|
+
var _a2;
|
|
4211
|
+
return String(event.chargeId || event.id) === String(((_a2 = connection2.source) == null ? void 0 : _a2.metered) || "");
|
|
4212
|
+
}
|
|
4213
|
+
)) == null ? void 0 : _c.occurredAt;
|
|
4214
|
+
reconciliation = [];
|
|
4215
|
+
let windowStart = ledgerFloor;
|
|
4216
|
+
for (const boundary of boundaries) {
|
|
4217
|
+
const windowEnd = new Date(boundary.occurredAt);
|
|
4218
|
+
const windowOpensAt = windowStart;
|
|
4219
|
+
windowStart = windowEnd;
|
|
4220
|
+
if (stampedAt && windowEnd <= new Date(stampedAt)) continue;
|
|
4221
|
+
const charged = Math.round(Number(boundary.usageQuantity) || 0);
|
|
4222
|
+
const [tally] = await read.aggregate({
|
|
4207
4223
|
collection: "order",
|
|
4208
|
-
|
|
4209
|
-
$
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4224
|
+
pipeline: [
|
|
4225
|
+
{ $match: {
|
|
4226
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4227
|
+
"billed.void": { $in: [null] },
|
|
4228
|
+
organization: workflow.organization,
|
|
4229
|
+
"provider.slug": "shopify"
|
|
4230
|
+
} },
|
|
4231
|
+
{ $group: {
|
|
4232
|
+
_id: null,
|
|
4233
|
+
billed: { $sum: "$billed.amount" },
|
|
4234
|
+
orders: { $push: "$id" }
|
|
4235
|
+
} }
|
|
4236
|
+
]
|
|
4237
|
+
}) || [];
|
|
4238
|
+
const billed = (tally == null ? void 0 : tally.billed) || 0;
|
|
4239
|
+
const orders = (tally == null ? void 0 : tally.orders) || [];
|
|
4240
|
+
const charge = String(boundary.chargeId || boundary.id);
|
|
4241
|
+
const balanced = charged === billed;
|
|
4242
|
+
if (!(charged || billed)) continue;
|
|
4243
|
+
if (balanced) {
|
|
4244
|
+
writes.push({
|
|
4245
|
+
collection: "order",
|
|
4246
|
+
data: {
|
|
4247
|
+
$set: { "billed.receipt": charge }
|
|
4248
|
+
},
|
|
4249
|
+
// Top-level, not an option — the controller takes
|
|
4250
|
+
// `multiple` beside `options`, and the shell
|
|
4251
|
+
// forwards it as such.
|
|
4252
|
+
multiple: true,
|
|
4253
|
+
operation: "update",
|
|
4254
|
+
query: {
|
|
4255
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4256
|
+
"billed.receipt": { $in: [null] },
|
|
4257
|
+
"billed.void": { $in: [null] },
|
|
4258
|
+
organization: workflow.organization,
|
|
4259
|
+
"provider.slug": "shopify"
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
} else {
|
|
4263
|
+
(_d = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _d.call(logger2, new Error(
|
|
4264
|
+
"shopify.usage.reconciliation.mismatch on shop " + connection2.shop + ": charge " + charge + " charged " + charged + " cents but its window holds " + billed + " cents across " + orders.length + " billed orders \u2014 each cent of difference is commission that never charged"
|
|
4265
|
+
), {
|
|
4266
|
+
extra: {
|
|
4267
|
+
billed,
|
|
4268
|
+
charge,
|
|
4269
|
+
charged,
|
|
4270
|
+
orders: orders.slice(0, 50),
|
|
4271
|
+
shop: connection2.shop,
|
|
4272
|
+
windowEnd,
|
|
4273
|
+
windowStart: windowOpensAt
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
}
|
|
4277
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt: boundary.occurredAt, orders: orders.length });
|
|
4223
4278
|
}
|
|
4224
4279
|
}
|
|
4225
4280
|
} catch (probeError) {
|
|
4226
4281
|
billingProbeFailed = true;
|
|
4227
|
-
(
|
|
4282
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4228
4283
|
connectionId: connection2.id,
|
|
4229
4284
|
message: probeError == null ? void 0 : probeError.message,
|
|
4230
4285
|
shop: connection2.shop
|
|
4231
4286
|
});
|
|
4232
4287
|
}
|
|
4233
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4288
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4234
4289
|
writes.push({
|
|
4235
4290
|
collection: "connection",
|
|
4236
4291
|
data: {
|
|
@@ -4301,6 +4356,9 @@ var shopify_default2 = {
|
|
|
4301
4356
|
metered: metered ?? null,
|
|
4302
4357
|
meterProbed: metered !== void 0,
|
|
4303
4358
|
pingedAt: /* @__PURE__ */ new Date(),
|
|
4359
|
+
// One entry per boundary charge judged this run: what
|
|
4360
|
+
// Shopify charged vs what the window's orders billed.
|
|
4361
|
+
...reconciliation && { reconciliation },
|
|
4304
4362
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4305
4363
|
refreshTokenRotated,
|
|
4306
4364
|
scopesMissing,
|
|
@@ -4931,6 +4931,9 @@ var shopify = {
|
|
|
4931
4931
|
let metered;
|
|
4932
4932
|
let shopCurrency = null;
|
|
4933
4933
|
let billingProbeFailed = false;
|
|
4934
|
+
// Per-boundary reconciliation results, surfaced in the step
|
|
4935
|
+
// response — the operator's ledger view of charged vs billed.
|
|
4936
|
+
let reconciliation = null;
|
|
4934
4937
|
|
|
4935
4938
|
try {
|
|
4936
4939
|
|
|
@@ -4971,71 +4974,162 @@ var shopify = {
|
|
|
4971
4974
|
// THE REAL METER SIGNAL — Partner API accrual. A CHARGE_USAGE
|
|
4972
4975
|
// event for this shop is proof billing works: its charge id
|
|
4973
4976
|
// becomes the stamp (overriding the line-item probe, which
|
|
4974
|
-
// managed pricing keeps blind)
|
|
4975
|
-
//
|
|
4976
|
-
//
|
|
4977
|
-
//
|
|
4978
|
-
// entirely until the provider row carries the credentials.
|
|
4977
|
+
// managed pricing keeps blind), and each charge row is a
|
|
4978
|
+
// billing BOUNDARY the reconciler below balances the books
|
|
4979
|
+
// against. Off entirely until the provider row carries the
|
|
4980
|
+
// credentials.
|
|
4979
4981
|
const partnerRow = read?.get ? await read.get({ collection : 'provider', query : { slug : 'shopify' } }) : null;
|
|
4980
4982
|
const partner = partnerRow?.settings ? decrypt( partnerRow.settings ) : {};
|
|
4981
4983
|
|
|
4982
4984
|
if( partner.partnerToken && partner.partnerOrgId && partner.partnerAppId && source?.id && shopify.partner?.getUsageChargeEvents ){
|
|
4983
4985
|
|
|
4986
|
+
const ledgerFloor = new Date( Date.now() - 30 * 24 * 60 * 60 * 1000 );
|
|
4987
|
+
|
|
4984
4988
|
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
4985
4989
|
appId : 'gid://shopify/App/' + partner.partnerAppId,
|
|
4986
|
-
first :
|
|
4987
|
-
occurredAtMin :
|
|
4990
|
+
first : 10,
|
|
4991
|
+
occurredAtMin : ledgerFloor.toISOString(),
|
|
4988
4992
|
organizationId : partner.partnerOrgId,
|
|
4989
4993
|
partnerToken : partner.partnerToken,
|
|
4990
4994
|
shopId : 'gid://shopify/Shop/' + source.id
|
|
4991
4995
|
});
|
|
4992
4996
|
|
|
4997
|
+
// Oldest first: each charge row is a boundary that closes
|
|
4998
|
+
// the window since the row before it.
|
|
4999
|
+
const boundaries = accruals
|
|
5000
|
+
.filter( ( event ) => event.occurredAt )
|
|
5001
|
+
.sort( ( a, b ) => new Date( a.occurredAt ) - new Date( b.occurredAt ) );
|
|
5002
|
+
|
|
4993
5003
|
// A usage ROW exists per subscription even before anything
|
|
4994
5004
|
// accrues (measured: a pre-meter approval shows qty 0), so
|
|
4995
5005
|
// the row is not the proof — a POSITIVE usageQuantity is.
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5006
|
+
// The LATEST positive row is the stamp.
|
|
5007
|
+
const accrued = [ ...boundaries ].reverse().find( ( event ) => Number( event.usageQuantity ) > 0 );
|
|
5008
|
+
|
|
5009
|
+
if( accrued ) metered = String( accrued.chargeId || accrued.id );
|
|
5010
|
+
|
|
5011
|
+
// THE BOUNDARY RECONCILER. Money truth lives at boundaries:
|
|
5012
|
+
// each charge row states what Shopify actually charged for
|
|
5013
|
+
// the window since the previous row, and the orders billed
|
|
5014
|
+
// inside that window state what we asked for — in the same
|
|
5015
|
+
// cents (billed.amount IS the value the usage event
|
|
5016
|
+
// carried). Balanced and positive → the window's orders get
|
|
5017
|
+
// the charge id as billed.receipt (their invoice bucket).
|
|
5018
|
+
// Mismatched → an alarm with the diff, and NO stamp: a
|
|
5019
|
+
// short window is money silently lost, which is the one
|
|
5020
|
+
// thing this whole chain exists to make loud.
|
|
5021
|
+
//
|
|
5022
|
+
// The stored stamp is the CURSOR: rows at or before the
|
|
5023
|
+
// charge already stamped were judged on the run that
|
|
5024
|
+
// stamped it, and re-judging them would page daily on the
|
|
5025
|
+
// pre-meter era's known losses. (A store with losses and no
|
|
5026
|
+
// positive row yet has no cursor, so its mismatch DOES
|
|
5027
|
+
// re-alarm daily — deliberately: that nag is what drives
|
|
5028
|
+
// the admin write-off, and voided orders leave the sums.)
|
|
5029
|
+
const stampedAt = boundaries.find(
|
|
5030
|
+
( event ) => String( event.chargeId || event.id ) === String( connection.source?.metered || '' )
|
|
5031
|
+
)?.occurredAt;
|
|
5032
|
+
|
|
5033
|
+
reconciliation = [];
|
|
5034
|
+
|
|
5035
|
+
let windowStart = ledgerFloor;
|
|
5036
|
+
|
|
5037
|
+
for( const boundary of boundaries ){
|
|
5038
|
+
|
|
5039
|
+
const windowEnd = new Date( boundary.occurredAt );
|
|
5040
|
+
const windowOpensAt = windowStart;
|
|
5041
|
+
|
|
5042
|
+
windowStart = windowEnd;
|
|
5043
|
+
|
|
5044
|
+
if( stampedAt && windowEnd <= new Date( stampedAt ) ) continue;
|
|
5045
|
+
|
|
5046
|
+
const charged = Math.round( Number( boundary.usageQuantity ) || 0 );
|
|
5047
|
+
|
|
5048
|
+
// Voided orders are written off — known unrecoverable,
|
|
5049
|
+
// excluded so they stop swaying every future boundary.
|
|
5050
|
+
// Already-receipted orders still COUNT in the sum (an
|
|
5051
|
+
// early stamp must not read as missing money) but are
|
|
5052
|
+
// never restamped.
|
|
5053
|
+
const [ tally ] = await read.aggregate({
|
|
5054
|
+
collection : 'order',
|
|
5055
|
+
pipeline : [
|
|
5056
|
+
{ $match : {
|
|
5057
|
+
'billed.date' : { $gt : windowOpensAt, $lte : windowEnd },
|
|
5058
|
+
'billed.void' : { $in : [ null ] },
|
|
5059
|
+
organization : workflow.organization,
|
|
5060
|
+
'provider.slug' : 'shopify'
|
|
5061
|
+
} },
|
|
5062
|
+
{ $group : {
|
|
5063
|
+
_id : null,
|
|
5064
|
+
billed : { $sum : '$billed.amount' },
|
|
5065
|
+
orders : { $push : '$id' }
|
|
5066
|
+
} }
|
|
5067
|
+
]
|
|
5068
|
+
}) || [];
|
|
5069
|
+
|
|
5070
|
+
const billed = tally?.billed || 0;
|
|
5071
|
+
const orders = tally?.orders || [];
|
|
5072
|
+
const charge = String( boundary.chargeId || boundary.id );
|
|
5073
|
+
const balanced = charged === billed;
|
|
5074
|
+
|
|
5075
|
+
if( ! ( charged || billed ) ) continue;
|
|
5076
|
+
|
|
5077
|
+
if( balanced ){
|
|
5078
|
+
|
|
5079
|
+
writes.push({
|
|
5080
|
+
collection : 'order',
|
|
5081
|
+
data : {
|
|
5082
|
+
$set : { 'billed.receipt' : charge }
|
|
5083
|
+
},
|
|
5084
|
+
// Top-level, not an option — the controller takes
|
|
5085
|
+
// `multiple` beside `options`, and the shell
|
|
5086
|
+
// forwards it as such.
|
|
5087
|
+
multiple : true,
|
|
5088
|
+
operation : 'update',
|
|
5089
|
+
query : {
|
|
5090
|
+
'billed.date' : { $gt : windowOpensAt, $lte : windowEnd },
|
|
5091
|
+
'billed.receipt' : { $in : [ null ] },
|
|
5092
|
+
'billed.void' : { $in : [ null ] },
|
|
5093
|
+
organization : workflow.organization,
|
|
5094
|
+
'provider.slug' : 'shopify'
|
|
5095
|
+
}
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
} else {
|
|
5099
|
+
|
|
5100
|
+
logger?.error?.( new Error(
|
|
5101
|
+
'shopify.usage.reconciliation.mismatch on shop ' + connection.shop
|
|
5102
|
+
+ ': charge ' + charge + ' charged ' + charged + ' cents but its window holds '
|
|
5103
|
+
+ billed + ' cents across ' + orders.length + ' billed orders'
|
|
5104
|
+
+ ' — each cent of difference is commission that never charged'
|
|
5105
|
+
), {
|
|
5106
|
+
extra : {
|
|
5107
|
+
billed,
|
|
5108
|
+
charge,
|
|
5109
|
+
charged,
|
|
5110
|
+
orders : orders.slice( 0, 50 ),
|
|
5111
|
+
shop : connection.shop,
|
|
5112
|
+
windowEnd,
|
|
5113
|
+
windowStart : windowOpensAt
|
|
5114
|
+
}
|
|
5115
|
+
});
|
|
4999
5116
|
|
|
5000
|
-
|
|
5117
|
+
}
|
|
5001
5118
|
|
|
5002
|
-
|
|
5003
|
-
// are per-subscription, not per-order, so the charge id
|
|
5004
|
-
// is the invoice bucket every order billed since that
|
|
5005
|
-
// subscription's approval rolled into — written onto
|
|
5006
|
-
// billed.receipt (many-update; $in [null] matches both
|
|
5007
|
-
// null and never-stamped). Orders billed before the
|
|
5008
|
-
// window belong to earlier, possibly unbillable
|
|
5009
|
-
// approvals and honestly stay unstamped.
|
|
5010
|
-
writes.push({
|
|
5011
|
-
collection : 'order',
|
|
5012
|
-
data : {
|
|
5013
|
-
$set : { 'billed.receipt' : metered }
|
|
5014
|
-
},
|
|
5015
|
-
// Top-level, not an option — the controller takes
|
|
5016
|
-
// `multiple` beside `options`, and the shell
|
|
5017
|
-
// forwards it as such.
|
|
5018
|
-
multiple : true,
|
|
5019
|
-
operation : 'update',
|
|
5020
|
-
query : {
|
|
5021
|
-
'billed.date' : { $gte : new Date( accrued.occurredAt || 0 ) },
|
|
5022
|
-
'billed.receipt' : { $in : [ null ] },
|
|
5023
|
-
organization : workflow.organization,
|
|
5024
|
-
'provider.slug' : 'shopify'
|
|
5025
|
-
}
|
|
5026
|
-
});
|
|
5119
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt : boundary.occurredAt, orders : orders.length });
|
|
5027
5120
|
|
|
5028
5121
|
}
|
|
5029
5122
|
|
|
5030
|
-
// NO
|
|
5031
|
-
//
|
|
5032
|
-
//
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
//
|
|
5036
|
-
//
|
|
5037
|
-
//
|
|
5038
|
-
//
|
|
5123
|
+
// DELIBERATELY NO null verdict from a lossy boundary. A
|
|
5124
|
+
// zero-charged window full of billed orders IS proof money
|
|
5125
|
+
// was lost — but it cannot say whether the CURRENT approval
|
|
5126
|
+
// is broken: the exact same shape appears the moment a
|
|
5127
|
+
// merchant fixes their store (the release charges out qty 0
|
|
5128
|
+
// and its window holds the pre-meter losses), and re-arming
|
|
5129
|
+
// "Fix order billing" on a store that was just fixed sends
|
|
5130
|
+
// the merchant through the flow again for nothing. The
|
|
5131
|
+
// mismatch alarm above owns that truth for the operator,
|
|
5132
|
+
// and the write-off drains it.
|
|
5039
5133
|
|
|
5040
5134
|
}
|
|
5041
5135
|
|
|
@@ -5154,6 +5248,9 @@ var shopify = {
|
|
|
5154
5248
|
metered : metered ?? null,
|
|
5155
5249
|
meterProbed : metered !== undefined,
|
|
5156
5250
|
pingedAt : new Date(),
|
|
5251
|
+
// One entry per boundary charge judged this run: what
|
|
5252
|
+
// Shopify charged vs what the window's orders billed.
|
|
5253
|
+
...( reconciliation && { reconciliation }),
|
|
5157
5254
|
refreshTokenExpiresAt : refreshTokenExpiresAt || null,
|
|
5158
5255
|
refreshTokenRotated,
|
|
5159
5256
|
scopesMissing,
|
|
@@ -4931,6 +4931,9 @@ var shopify = {
|
|
|
4931
4931
|
let metered;
|
|
4932
4932
|
let shopCurrency = null;
|
|
4933
4933
|
let billingProbeFailed = false;
|
|
4934
|
+
// Per-boundary reconciliation results, surfaced in the step
|
|
4935
|
+
// response — the operator's ledger view of charged vs billed.
|
|
4936
|
+
let reconciliation = null;
|
|
4934
4937
|
|
|
4935
4938
|
try {
|
|
4936
4939
|
|
|
@@ -4971,71 +4974,162 @@ var shopify = {
|
|
|
4971
4974
|
// THE REAL METER SIGNAL — Partner API accrual. A CHARGE_USAGE
|
|
4972
4975
|
// event for this shop is proof billing works: its charge id
|
|
4973
4976
|
// becomes the stamp (overriding the line-item probe, which
|
|
4974
|
-
// managed pricing keeps blind)
|
|
4975
|
-
//
|
|
4976
|
-
//
|
|
4977
|
-
//
|
|
4978
|
-
// entirely until the provider row carries the credentials.
|
|
4977
|
+
// managed pricing keeps blind), and each charge row is a
|
|
4978
|
+
// billing BOUNDARY the reconciler below balances the books
|
|
4979
|
+
// against. Off entirely until the provider row carries the
|
|
4980
|
+
// credentials.
|
|
4979
4981
|
const partnerRow = read?.get ? await read.get({ collection : 'provider', query : { slug : 'shopify' } }) : null;
|
|
4980
4982
|
const partner = partnerRow?.settings ? decrypt( partnerRow.settings ) : {};
|
|
4981
4983
|
|
|
4982
4984
|
if( partner.partnerToken && partner.partnerOrgId && partner.partnerAppId && source?.id && shopify.partner?.getUsageChargeEvents ){
|
|
4983
4985
|
|
|
4986
|
+
const ledgerFloor = new Date( Date.now() - 30 * 24 * 60 * 60 * 1000 );
|
|
4987
|
+
|
|
4984
4988
|
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
4985
4989
|
appId : 'gid://shopify/App/' + partner.partnerAppId,
|
|
4986
|
-
first :
|
|
4987
|
-
occurredAtMin :
|
|
4990
|
+
first : 10,
|
|
4991
|
+
occurredAtMin : ledgerFloor.toISOString(),
|
|
4988
4992
|
organizationId : partner.partnerOrgId,
|
|
4989
4993
|
partnerToken : partner.partnerToken,
|
|
4990
4994
|
shopId : 'gid://shopify/Shop/' + source.id
|
|
4991
4995
|
});
|
|
4992
4996
|
|
|
4997
|
+
// Oldest first: each charge row is a boundary that closes
|
|
4998
|
+
// the window since the row before it.
|
|
4999
|
+
const boundaries = accruals
|
|
5000
|
+
.filter( ( event ) => event.occurredAt )
|
|
5001
|
+
.sort( ( a, b ) => new Date( a.occurredAt ) - new Date( b.occurredAt ) );
|
|
5002
|
+
|
|
4993
5003
|
// A usage ROW exists per subscription even before anything
|
|
4994
5004
|
// accrues (measured: a pre-meter approval shows qty 0), so
|
|
4995
5005
|
// the row is not the proof — a POSITIVE usageQuantity is.
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
5006
|
+
// The LATEST positive row is the stamp.
|
|
5007
|
+
const accrued = [ ...boundaries ].reverse().find( ( event ) => Number( event.usageQuantity ) > 0 );
|
|
5008
|
+
|
|
5009
|
+
if( accrued ) metered = String( accrued.chargeId || accrued.id );
|
|
5010
|
+
|
|
5011
|
+
// THE BOUNDARY RECONCILER. Money truth lives at boundaries:
|
|
5012
|
+
// each charge row states what Shopify actually charged for
|
|
5013
|
+
// the window since the previous row, and the orders billed
|
|
5014
|
+
// inside that window state what we asked for — in the same
|
|
5015
|
+
// cents (billed.amount IS the value the usage event
|
|
5016
|
+
// carried). Balanced and positive → the window's orders get
|
|
5017
|
+
// the charge id as billed.receipt (their invoice bucket).
|
|
5018
|
+
// Mismatched → an alarm with the diff, and NO stamp: a
|
|
5019
|
+
// short window is money silently lost, which is the one
|
|
5020
|
+
// thing this whole chain exists to make loud.
|
|
5021
|
+
//
|
|
5022
|
+
// The stored stamp is the CURSOR: rows at or before the
|
|
5023
|
+
// charge already stamped were judged on the run that
|
|
5024
|
+
// stamped it, and re-judging them would page daily on the
|
|
5025
|
+
// pre-meter era's known losses. (A store with losses and no
|
|
5026
|
+
// positive row yet has no cursor, so its mismatch DOES
|
|
5027
|
+
// re-alarm daily — deliberately: that nag is what drives
|
|
5028
|
+
// the admin write-off, and voided orders leave the sums.)
|
|
5029
|
+
const stampedAt = boundaries.find(
|
|
5030
|
+
( event ) => String( event.chargeId || event.id ) === String( connection.source?.metered || '' )
|
|
5031
|
+
)?.occurredAt;
|
|
5032
|
+
|
|
5033
|
+
reconciliation = [];
|
|
5034
|
+
|
|
5035
|
+
let windowStart = ledgerFloor;
|
|
5036
|
+
|
|
5037
|
+
for( const boundary of boundaries ){
|
|
5038
|
+
|
|
5039
|
+
const windowEnd = new Date( boundary.occurredAt );
|
|
5040
|
+
const windowOpensAt = windowStart;
|
|
5041
|
+
|
|
5042
|
+
windowStart = windowEnd;
|
|
5043
|
+
|
|
5044
|
+
if( stampedAt && windowEnd <= new Date( stampedAt ) ) continue;
|
|
5045
|
+
|
|
5046
|
+
const charged = Math.round( Number( boundary.usageQuantity ) || 0 );
|
|
5047
|
+
|
|
5048
|
+
// Voided orders are written off — known unrecoverable,
|
|
5049
|
+
// excluded so they stop swaying every future boundary.
|
|
5050
|
+
// Already-receipted orders still COUNT in the sum (an
|
|
5051
|
+
// early stamp must not read as missing money) but are
|
|
5052
|
+
// never restamped.
|
|
5053
|
+
const [ tally ] = await read.aggregate({
|
|
5054
|
+
collection : 'order',
|
|
5055
|
+
pipeline : [
|
|
5056
|
+
{ $match : {
|
|
5057
|
+
'billed.date' : { $gt : windowOpensAt, $lte : windowEnd },
|
|
5058
|
+
'billed.void' : { $in : [ null ] },
|
|
5059
|
+
organization : workflow.organization,
|
|
5060
|
+
'provider.slug' : 'shopify'
|
|
5061
|
+
} },
|
|
5062
|
+
{ $group : {
|
|
5063
|
+
_id : null,
|
|
5064
|
+
billed : { $sum : '$billed.amount' },
|
|
5065
|
+
orders : { $push : '$id' }
|
|
5066
|
+
} }
|
|
5067
|
+
]
|
|
5068
|
+
}) || [];
|
|
5069
|
+
|
|
5070
|
+
const billed = tally?.billed || 0;
|
|
5071
|
+
const orders = tally?.orders || [];
|
|
5072
|
+
const charge = String( boundary.chargeId || boundary.id );
|
|
5073
|
+
const balanced = charged === billed;
|
|
5074
|
+
|
|
5075
|
+
if( ! ( charged || billed ) ) continue;
|
|
5076
|
+
|
|
5077
|
+
if( balanced ){
|
|
5078
|
+
|
|
5079
|
+
writes.push({
|
|
5080
|
+
collection : 'order',
|
|
5081
|
+
data : {
|
|
5082
|
+
$set : { 'billed.receipt' : charge }
|
|
5083
|
+
},
|
|
5084
|
+
// Top-level, not an option — the controller takes
|
|
5085
|
+
// `multiple` beside `options`, and the shell
|
|
5086
|
+
// forwards it as such.
|
|
5087
|
+
multiple : true,
|
|
5088
|
+
operation : 'update',
|
|
5089
|
+
query : {
|
|
5090
|
+
'billed.date' : { $gt : windowOpensAt, $lte : windowEnd },
|
|
5091
|
+
'billed.receipt' : { $in : [ null ] },
|
|
5092
|
+
'billed.void' : { $in : [ null ] },
|
|
5093
|
+
organization : workflow.organization,
|
|
5094
|
+
'provider.slug' : 'shopify'
|
|
5095
|
+
}
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
} else {
|
|
5099
|
+
|
|
5100
|
+
logger?.error?.( new Error(
|
|
5101
|
+
'shopify.usage.reconciliation.mismatch on shop ' + connection.shop
|
|
5102
|
+
+ ': charge ' + charge + ' charged ' + charged + ' cents but its window holds '
|
|
5103
|
+
+ billed + ' cents across ' + orders.length + ' billed orders'
|
|
5104
|
+
+ ' — each cent of difference is commission that never charged'
|
|
5105
|
+
), {
|
|
5106
|
+
extra : {
|
|
5107
|
+
billed,
|
|
5108
|
+
charge,
|
|
5109
|
+
charged,
|
|
5110
|
+
orders : orders.slice( 0, 50 ),
|
|
5111
|
+
shop : connection.shop,
|
|
5112
|
+
windowEnd,
|
|
5113
|
+
windowStart : windowOpensAt
|
|
5114
|
+
}
|
|
5115
|
+
});
|
|
4999
5116
|
|
|
5000
|
-
|
|
5117
|
+
}
|
|
5001
5118
|
|
|
5002
|
-
|
|
5003
|
-
// are per-subscription, not per-order, so the charge id
|
|
5004
|
-
// is the invoice bucket every order billed since that
|
|
5005
|
-
// subscription's approval rolled into — written onto
|
|
5006
|
-
// billed.receipt (many-update; $in [null] matches both
|
|
5007
|
-
// null and never-stamped). Orders billed before the
|
|
5008
|
-
// window belong to earlier, possibly unbillable
|
|
5009
|
-
// approvals and honestly stay unstamped.
|
|
5010
|
-
writes.push({
|
|
5011
|
-
collection : 'order',
|
|
5012
|
-
data : {
|
|
5013
|
-
$set : { 'billed.receipt' : metered }
|
|
5014
|
-
},
|
|
5015
|
-
// Top-level, not an option — the controller takes
|
|
5016
|
-
// `multiple` beside `options`, and the shell
|
|
5017
|
-
// forwards it as such.
|
|
5018
|
-
multiple : true,
|
|
5019
|
-
operation : 'update',
|
|
5020
|
-
query : {
|
|
5021
|
-
'billed.date' : { $gte : new Date( accrued.occurredAt || 0 ) },
|
|
5022
|
-
'billed.receipt' : { $in : [ null ] },
|
|
5023
|
-
organization : workflow.organization,
|
|
5024
|
-
'provider.slug' : 'shopify'
|
|
5025
|
-
}
|
|
5026
|
-
});
|
|
5119
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt : boundary.occurredAt, orders : orders.length });
|
|
5027
5120
|
|
|
5028
5121
|
}
|
|
5029
5122
|
|
|
5030
|
-
// NO
|
|
5031
|
-
//
|
|
5032
|
-
//
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
//
|
|
5036
|
-
//
|
|
5037
|
-
//
|
|
5038
|
-
//
|
|
5123
|
+
// DELIBERATELY NO null verdict from a lossy boundary. A
|
|
5124
|
+
// zero-charged window full of billed orders IS proof money
|
|
5125
|
+
// was lost — but it cannot say whether the CURRENT approval
|
|
5126
|
+
// is broken: the exact same shape appears the moment a
|
|
5127
|
+
// merchant fixes their store (the release charges out qty 0
|
|
5128
|
+
// and its window holds the pre-meter losses), and re-arming
|
|
5129
|
+
// "Fix order billing" on a store that was just fixed sends
|
|
5130
|
+
// the merchant through the flow again for nothing. The
|
|
5131
|
+
// mismatch alarm above owns that truth for the operator,
|
|
5132
|
+
// and the write-off drains it.
|
|
5039
5133
|
|
|
5040
5134
|
}
|
|
5041
5135
|
|
|
@@ -5154,6 +5248,9 @@ var shopify = {
|
|
|
5154
5248
|
metered : metered ?? null,
|
|
5155
5249
|
meterProbed : metered !== undefined,
|
|
5156
5250
|
pingedAt : new Date(),
|
|
5251
|
+
// One entry per boundary charge judged this run: what
|
|
5252
|
+
// Shopify charged vs what the window's orders billed.
|
|
5253
|
+
...( reconciliation && { reconciliation }),
|
|
5157
5254
|
refreshTokenExpiresAt : refreshTokenExpiresAt || null,
|
|
5158
5255
|
refreshTokenRotated,
|
|
5159
5256
|
scopesMissing,
|
|
@@ -4073,7 +4073,7 @@ var shopify_default2 = {
|
|
|
4073
4073
|
// against the ones the app now needs, and queues a webhook
|
|
4074
4074
|
// reconciliation.
|
|
4075
4075
|
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
4076
|
-
var _a, _b, _c, _d;
|
|
4076
|
+
var _a, _b, _c, _d, _e, _f;
|
|
4077
4077
|
const request2 = {
|
|
4078
4078
|
connectionId: workflow.connection,
|
|
4079
4079
|
organizationId: workflow.organization,
|
|
@@ -4098,6 +4098,7 @@ var shopify_default2 = {
|
|
|
4098
4098
|
let metered;
|
|
4099
4099
|
let shopCurrency = null;
|
|
4100
4100
|
let billingProbeFailed = false;
|
|
4101
|
+
let reconciliation = null;
|
|
4101
4102
|
try {
|
|
4102
4103
|
if (!(source == null ? void 0 : source.id)) {
|
|
4103
4104
|
const shopData = await shopify.oauth.getShop({ adminAccessToken, shop: connection2.shop });
|
|
@@ -4118,45 +4119,99 @@ var shopify_default2 = {
|
|
|
4118
4119
|
const partnerRow = (read == null ? void 0 : read.get) ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
4119
4120
|
const partner = (partnerRow == null ? void 0 : partnerRow.settings) ? decrypt(partnerRow.settings) : {};
|
|
4120
4121
|
if (partner.partnerToken && partner.partnerOrgId && partner.partnerAppId && (source == null ? void 0 : source.id) && ((_b = shopify.partner) == null ? void 0 : _b.getUsageChargeEvents)) {
|
|
4122
|
+
const ledgerFloor = new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
4121
4123
|
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
4122
4124
|
appId: "gid://shopify/App/" + partner.partnerAppId,
|
|
4123
|
-
first:
|
|
4124
|
-
occurredAtMin:
|
|
4125
|
+
first: 10,
|
|
4126
|
+
occurredAtMin: ledgerFloor.toISOString(),
|
|
4125
4127
|
organizationId: partner.partnerOrgId,
|
|
4126
4128
|
partnerToken: partner.partnerToken,
|
|
4127
4129
|
shopId: "gid://shopify/Shop/" + source.id
|
|
4128
4130
|
});
|
|
4129
|
-
const
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4131
|
+
const boundaries = accruals.filter((event) => event.occurredAt).sort((a, b) => new Date(a.occurredAt) - new Date(b.occurredAt));
|
|
4132
|
+
const accrued = [...boundaries].reverse().find((event) => Number(event.usageQuantity) > 0);
|
|
4133
|
+
if (accrued) metered = String(accrued.chargeId || accrued.id);
|
|
4134
|
+
const stampedAt = (_c = boundaries.find(
|
|
4135
|
+
(event) => {
|
|
4136
|
+
var _a2;
|
|
4137
|
+
return String(event.chargeId || event.id) === String(((_a2 = connection2.source) == null ? void 0 : _a2.metered) || "");
|
|
4138
|
+
}
|
|
4139
|
+
)) == null ? void 0 : _c.occurredAt;
|
|
4140
|
+
reconciliation = [];
|
|
4141
|
+
let windowStart = ledgerFloor;
|
|
4142
|
+
for (const boundary of boundaries) {
|
|
4143
|
+
const windowEnd = new Date(boundary.occurredAt);
|
|
4144
|
+
const windowOpensAt = windowStart;
|
|
4145
|
+
windowStart = windowEnd;
|
|
4146
|
+
if (stampedAt && windowEnd <= new Date(stampedAt)) continue;
|
|
4147
|
+
const charged = Math.round(Number(boundary.usageQuantity) || 0);
|
|
4148
|
+
const [tally] = await read.aggregate({
|
|
4133
4149
|
collection: "order",
|
|
4134
|
-
|
|
4135
|
-
$
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4150
|
+
pipeline: [
|
|
4151
|
+
{ $match: {
|
|
4152
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4153
|
+
"billed.void": { $in: [null] },
|
|
4154
|
+
organization: workflow.organization,
|
|
4155
|
+
"provider.slug": "shopify"
|
|
4156
|
+
} },
|
|
4157
|
+
{ $group: {
|
|
4158
|
+
_id: null,
|
|
4159
|
+
billed: { $sum: "$billed.amount" },
|
|
4160
|
+
orders: { $push: "$id" }
|
|
4161
|
+
} }
|
|
4162
|
+
]
|
|
4163
|
+
}) || [];
|
|
4164
|
+
const billed = (tally == null ? void 0 : tally.billed) || 0;
|
|
4165
|
+
const orders = (tally == null ? void 0 : tally.orders) || [];
|
|
4166
|
+
const charge = String(boundary.chargeId || boundary.id);
|
|
4167
|
+
const balanced = charged === billed;
|
|
4168
|
+
if (!(charged || billed)) continue;
|
|
4169
|
+
if (balanced) {
|
|
4170
|
+
writes.push({
|
|
4171
|
+
collection: "order",
|
|
4172
|
+
data: {
|
|
4173
|
+
$set: { "billed.receipt": charge }
|
|
4174
|
+
},
|
|
4175
|
+
// Top-level, not an option — the controller takes
|
|
4176
|
+
// `multiple` beside `options`, and the shell
|
|
4177
|
+
// forwards it as such.
|
|
4178
|
+
multiple: true,
|
|
4179
|
+
operation: "update",
|
|
4180
|
+
query: {
|
|
4181
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4182
|
+
"billed.receipt": { $in: [null] },
|
|
4183
|
+
"billed.void": { $in: [null] },
|
|
4184
|
+
organization: workflow.organization,
|
|
4185
|
+
"provider.slug": "shopify"
|
|
4186
|
+
}
|
|
4187
|
+
});
|
|
4188
|
+
} else {
|
|
4189
|
+
(_d = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _d.call(logger2, new Error(
|
|
4190
|
+
"shopify.usage.reconciliation.mismatch on shop " + connection2.shop + ": charge " + charge + " charged " + charged + " cents but its window holds " + billed + " cents across " + orders.length + " billed orders \u2014 each cent of difference is commission that never charged"
|
|
4191
|
+
), {
|
|
4192
|
+
extra: {
|
|
4193
|
+
billed,
|
|
4194
|
+
charge,
|
|
4195
|
+
charged,
|
|
4196
|
+
orders: orders.slice(0, 50),
|
|
4197
|
+
shop: connection2.shop,
|
|
4198
|
+
windowEnd,
|
|
4199
|
+
windowStart: windowOpensAt
|
|
4200
|
+
}
|
|
4201
|
+
});
|
|
4202
|
+
}
|
|
4203
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt: boundary.occurredAt, orders: orders.length });
|
|
4149
4204
|
}
|
|
4150
4205
|
}
|
|
4151
4206
|
} catch (probeError) {
|
|
4152
4207
|
billingProbeFailed = true;
|
|
4153
|
-
(
|
|
4208
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4154
4209
|
connectionId: connection2.id,
|
|
4155
4210
|
message: probeError == null ? void 0 : probeError.message,
|
|
4156
4211
|
shop: connection2.shop
|
|
4157
4212
|
});
|
|
4158
4213
|
}
|
|
4159
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4214
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4160
4215
|
writes.push({
|
|
4161
4216
|
collection: "connection",
|
|
4162
4217
|
data: {
|
|
@@ -4227,6 +4282,9 @@ var shopify_default2 = {
|
|
|
4227
4282
|
metered: metered ?? null,
|
|
4228
4283
|
meterProbed: metered !== void 0,
|
|
4229
4284
|
pingedAt: /* @__PURE__ */ new Date(),
|
|
4285
|
+
// One entry per boundary charge judged this run: what
|
|
4286
|
+
// Shopify charged vs what the window's orders billed.
|
|
4287
|
+
...reconciliation && { reconciliation },
|
|
4230
4288
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4231
4289
|
refreshTokenRotated,
|
|
4232
4290
|
scopesMissing,
|
package/dist/providers.cjs
CHANGED
|
@@ -4072,7 +4072,7 @@ var shopify_default2 = {
|
|
|
4072
4072
|
// against the ones the app now needs, and queues a webhook
|
|
4073
4073
|
// reconciliation.
|
|
4074
4074
|
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
4075
|
-
var _a, _b, _c, _d;
|
|
4075
|
+
var _a, _b, _c, _d, _e, _f;
|
|
4076
4076
|
const request2 = {
|
|
4077
4077
|
connectionId: workflow.connection,
|
|
4078
4078
|
organizationId: workflow.organization,
|
|
@@ -4097,6 +4097,7 @@ var shopify_default2 = {
|
|
|
4097
4097
|
let metered;
|
|
4098
4098
|
let shopCurrency = null;
|
|
4099
4099
|
let billingProbeFailed = false;
|
|
4100
|
+
let reconciliation = null;
|
|
4100
4101
|
try {
|
|
4101
4102
|
if (!(source == null ? void 0 : source.id)) {
|
|
4102
4103
|
const shopData = await shopify.oauth.getShop({ adminAccessToken, shop: connection2.shop });
|
|
@@ -4117,45 +4118,99 @@ var shopify_default2 = {
|
|
|
4117
4118
|
const partnerRow = (read == null ? void 0 : read.get) ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
4118
4119
|
const partner = (partnerRow == null ? void 0 : partnerRow.settings) ? decrypt(partnerRow.settings) : {};
|
|
4119
4120
|
if (partner.partnerToken && partner.partnerOrgId && partner.partnerAppId && (source == null ? void 0 : source.id) && ((_b = shopify.partner) == null ? void 0 : _b.getUsageChargeEvents)) {
|
|
4121
|
+
const ledgerFloor = new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
4120
4122
|
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
4121
4123
|
appId: "gid://shopify/App/" + partner.partnerAppId,
|
|
4122
|
-
first:
|
|
4123
|
-
occurredAtMin:
|
|
4124
|
+
first: 10,
|
|
4125
|
+
occurredAtMin: ledgerFloor.toISOString(),
|
|
4124
4126
|
organizationId: partner.partnerOrgId,
|
|
4125
4127
|
partnerToken: partner.partnerToken,
|
|
4126
4128
|
shopId: "gid://shopify/Shop/" + source.id
|
|
4127
4129
|
});
|
|
4128
|
-
const
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4130
|
+
const boundaries = accruals.filter((event) => event.occurredAt).sort((a, b) => new Date(a.occurredAt) - new Date(b.occurredAt));
|
|
4131
|
+
const accrued = [...boundaries].reverse().find((event) => Number(event.usageQuantity) > 0);
|
|
4132
|
+
if (accrued) metered = String(accrued.chargeId || accrued.id);
|
|
4133
|
+
const stampedAt = (_c = boundaries.find(
|
|
4134
|
+
(event) => {
|
|
4135
|
+
var _a2;
|
|
4136
|
+
return String(event.chargeId || event.id) === String(((_a2 = connection2.source) == null ? void 0 : _a2.metered) || "");
|
|
4137
|
+
}
|
|
4138
|
+
)) == null ? void 0 : _c.occurredAt;
|
|
4139
|
+
reconciliation = [];
|
|
4140
|
+
let windowStart = ledgerFloor;
|
|
4141
|
+
for (const boundary of boundaries) {
|
|
4142
|
+
const windowEnd = new Date(boundary.occurredAt);
|
|
4143
|
+
const windowOpensAt = windowStart;
|
|
4144
|
+
windowStart = windowEnd;
|
|
4145
|
+
if (stampedAt && windowEnd <= new Date(stampedAt)) continue;
|
|
4146
|
+
const charged = Math.round(Number(boundary.usageQuantity) || 0);
|
|
4147
|
+
const [tally] = await read.aggregate({
|
|
4132
4148
|
collection: "order",
|
|
4133
|
-
|
|
4134
|
-
$
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4149
|
+
pipeline: [
|
|
4150
|
+
{ $match: {
|
|
4151
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4152
|
+
"billed.void": { $in: [null] },
|
|
4153
|
+
organization: workflow.organization,
|
|
4154
|
+
"provider.slug": "shopify"
|
|
4155
|
+
} },
|
|
4156
|
+
{ $group: {
|
|
4157
|
+
_id: null,
|
|
4158
|
+
billed: { $sum: "$billed.amount" },
|
|
4159
|
+
orders: { $push: "$id" }
|
|
4160
|
+
} }
|
|
4161
|
+
]
|
|
4162
|
+
}) || [];
|
|
4163
|
+
const billed = (tally == null ? void 0 : tally.billed) || 0;
|
|
4164
|
+
const orders = (tally == null ? void 0 : tally.orders) || [];
|
|
4165
|
+
const charge = String(boundary.chargeId || boundary.id);
|
|
4166
|
+
const balanced = charged === billed;
|
|
4167
|
+
if (!(charged || billed)) continue;
|
|
4168
|
+
if (balanced) {
|
|
4169
|
+
writes.push({
|
|
4170
|
+
collection: "order",
|
|
4171
|
+
data: {
|
|
4172
|
+
$set: { "billed.receipt": charge }
|
|
4173
|
+
},
|
|
4174
|
+
// Top-level, not an option — the controller takes
|
|
4175
|
+
// `multiple` beside `options`, and the shell
|
|
4176
|
+
// forwards it as such.
|
|
4177
|
+
multiple: true,
|
|
4178
|
+
operation: "update",
|
|
4179
|
+
query: {
|
|
4180
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4181
|
+
"billed.receipt": { $in: [null] },
|
|
4182
|
+
"billed.void": { $in: [null] },
|
|
4183
|
+
organization: workflow.organization,
|
|
4184
|
+
"provider.slug": "shopify"
|
|
4185
|
+
}
|
|
4186
|
+
});
|
|
4187
|
+
} else {
|
|
4188
|
+
(_d = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _d.call(logger2, new Error(
|
|
4189
|
+
"shopify.usage.reconciliation.mismatch on shop " + connection2.shop + ": charge " + charge + " charged " + charged + " cents but its window holds " + billed + " cents across " + orders.length + " billed orders \u2014 each cent of difference is commission that never charged"
|
|
4190
|
+
), {
|
|
4191
|
+
extra: {
|
|
4192
|
+
billed,
|
|
4193
|
+
charge,
|
|
4194
|
+
charged,
|
|
4195
|
+
orders: orders.slice(0, 50),
|
|
4196
|
+
shop: connection2.shop,
|
|
4197
|
+
windowEnd,
|
|
4198
|
+
windowStart: windowOpensAt
|
|
4199
|
+
}
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt: boundary.occurredAt, orders: orders.length });
|
|
4148
4203
|
}
|
|
4149
4204
|
}
|
|
4150
4205
|
} catch (probeError) {
|
|
4151
4206
|
billingProbeFailed = true;
|
|
4152
|
-
(
|
|
4207
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4153
4208
|
connectionId: connection2.id,
|
|
4154
4209
|
message: probeError == null ? void 0 : probeError.message,
|
|
4155
4210
|
shop: connection2.shop
|
|
4156
4211
|
});
|
|
4157
4212
|
}
|
|
4158
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4213
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4159
4214
|
writes.push({
|
|
4160
4215
|
collection: "connection",
|
|
4161
4216
|
data: {
|
|
@@ -4226,6 +4281,9 @@ var shopify_default2 = {
|
|
|
4226
4281
|
metered: metered ?? null,
|
|
4227
4282
|
meterProbed: metered !== void 0,
|
|
4228
4283
|
pingedAt: /* @__PURE__ */ new Date(),
|
|
4284
|
+
// One entry per boundary charge judged this run: what
|
|
4285
|
+
// Shopify charged vs what the window's orders billed.
|
|
4286
|
+
...reconciliation && { reconciliation },
|
|
4229
4287
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4230
4288
|
refreshTokenRotated,
|
|
4231
4289
|
scopesMissing,
|
package/dist/providers.js
CHANGED
|
@@ -4028,7 +4028,7 @@ var shopify_default2 = {
|
|
|
4028
4028
|
// against the ones the app now needs, and queues a webhook
|
|
4029
4029
|
// reconciliation.
|
|
4030
4030
|
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
4031
|
-
var _a, _b, _c, _d;
|
|
4031
|
+
var _a, _b, _c, _d, _e, _f;
|
|
4032
4032
|
const request2 = {
|
|
4033
4033
|
connectionId: workflow.connection,
|
|
4034
4034
|
organizationId: workflow.organization,
|
|
@@ -4053,6 +4053,7 @@ var shopify_default2 = {
|
|
|
4053
4053
|
let metered;
|
|
4054
4054
|
let shopCurrency = null;
|
|
4055
4055
|
let billingProbeFailed = false;
|
|
4056
|
+
let reconciliation = null;
|
|
4056
4057
|
try {
|
|
4057
4058
|
if (!(source == null ? void 0 : source.id)) {
|
|
4058
4059
|
const shopData = await shopify.oauth.getShop({ adminAccessToken, shop: connection2.shop });
|
|
@@ -4073,45 +4074,99 @@ var shopify_default2 = {
|
|
|
4073
4074
|
const partnerRow = (read == null ? void 0 : read.get) ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
4074
4075
|
const partner = (partnerRow == null ? void 0 : partnerRow.settings) ? decrypt(partnerRow.settings) : {};
|
|
4075
4076
|
if (partner.partnerToken && partner.partnerOrgId && partner.partnerAppId && (source == null ? void 0 : source.id) && ((_b = shopify.partner) == null ? void 0 : _b.getUsageChargeEvents)) {
|
|
4077
|
+
const ledgerFloor = new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
4076
4078
|
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
4077
4079
|
appId: "gid://shopify/App/" + partner.partnerAppId,
|
|
4078
|
-
first:
|
|
4079
|
-
occurredAtMin:
|
|
4080
|
+
first: 10,
|
|
4081
|
+
occurredAtMin: ledgerFloor.toISOString(),
|
|
4080
4082
|
organizationId: partner.partnerOrgId,
|
|
4081
4083
|
partnerToken: partner.partnerToken,
|
|
4082
4084
|
shopId: "gid://shopify/Shop/" + source.id
|
|
4083
4085
|
});
|
|
4084
|
-
const
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4086
|
+
const boundaries = accruals.filter((event) => event.occurredAt).sort((a, b) => new Date(a.occurredAt) - new Date(b.occurredAt));
|
|
4087
|
+
const accrued = [...boundaries].reverse().find((event) => Number(event.usageQuantity) > 0);
|
|
4088
|
+
if (accrued) metered = String(accrued.chargeId || accrued.id);
|
|
4089
|
+
const stampedAt = (_c = boundaries.find(
|
|
4090
|
+
(event) => {
|
|
4091
|
+
var _a2;
|
|
4092
|
+
return String(event.chargeId || event.id) === String(((_a2 = connection2.source) == null ? void 0 : _a2.metered) || "");
|
|
4093
|
+
}
|
|
4094
|
+
)) == null ? void 0 : _c.occurredAt;
|
|
4095
|
+
reconciliation = [];
|
|
4096
|
+
let windowStart = ledgerFloor;
|
|
4097
|
+
for (const boundary of boundaries) {
|
|
4098
|
+
const windowEnd = new Date(boundary.occurredAt);
|
|
4099
|
+
const windowOpensAt = windowStart;
|
|
4100
|
+
windowStart = windowEnd;
|
|
4101
|
+
if (stampedAt && windowEnd <= new Date(stampedAt)) continue;
|
|
4102
|
+
const charged = Math.round(Number(boundary.usageQuantity) || 0);
|
|
4103
|
+
const [tally] = await read.aggregate({
|
|
4088
4104
|
collection: "order",
|
|
4089
|
-
|
|
4090
|
-
$
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4105
|
+
pipeline: [
|
|
4106
|
+
{ $match: {
|
|
4107
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4108
|
+
"billed.void": { $in: [null] },
|
|
4109
|
+
organization: workflow.organization,
|
|
4110
|
+
"provider.slug": "shopify"
|
|
4111
|
+
} },
|
|
4112
|
+
{ $group: {
|
|
4113
|
+
_id: null,
|
|
4114
|
+
billed: { $sum: "$billed.amount" },
|
|
4115
|
+
orders: { $push: "$id" }
|
|
4116
|
+
} }
|
|
4117
|
+
]
|
|
4118
|
+
}) || [];
|
|
4119
|
+
const billed = (tally == null ? void 0 : tally.billed) || 0;
|
|
4120
|
+
const orders = (tally == null ? void 0 : tally.orders) || [];
|
|
4121
|
+
const charge = String(boundary.chargeId || boundary.id);
|
|
4122
|
+
const balanced = charged === billed;
|
|
4123
|
+
if (!(charged || billed)) continue;
|
|
4124
|
+
if (balanced) {
|
|
4125
|
+
writes.push({
|
|
4126
|
+
collection: "order",
|
|
4127
|
+
data: {
|
|
4128
|
+
$set: { "billed.receipt": charge }
|
|
4129
|
+
},
|
|
4130
|
+
// Top-level, not an option — the controller takes
|
|
4131
|
+
// `multiple` beside `options`, and the shell
|
|
4132
|
+
// forwards it as such.
|
|
4133
|
+
multiple: true,
|
|
4134
|
+
operation: "update",
|
|
4135
|
+
query: {
|
|
4136
|
+
"billed.date": { $gt: windowOpensAt, $lte: windowEnd },
|
|
4137
|
+
"billed.receipt": { $in: [null] },
|
|
4138
|
+
"billed.void": { $in: [null] },
|
|
4139
|
+
organization: workflow.organization,
|
|
4140
|
+
"provider.slug": "shopify"
|
|
4141
|
+
}
|
|
4142
|
+
});
|
|
4143
|
+
} else {
|
|
4144
|
+
(_d = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _d.call(logger2, new Error(
|
|
4145
|
+
"shopify.usage.reconciliation.mismatch on shop " + connection2.shop + ": charge " + charge + " charged " + charged + " cents but its window holds " + billed + " cents across " + orders.length + " billed orders \u2014 each cent of difference is commission that never charged"
|
|
4146
|
+
), {
|
|
4147
|
+
extra: {
|
|
4148
|
+
billed,
|
|
4149
|
+
charge,
|
|
4150
|
+
charged,
|
|
4151
|
+
orders: orders.slice(0, 50),
|
|
4152
|
+
shop: connection2.shop,
|
|
4153
|
+
windowEnd,
|
|
4154
|
+
windowStart: windowOpensAt
|
|
4155
|
+
}
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt: boundary.occurredAt, orders: orders.length });
|
|
4104
4159
|
}
|
|
4105
4160
|
}
|
|
4106
4161
|
} catch (probeError) {
|
|
4107
4162
|
billingProbeFailed = true;
|
|
4108
|
-
(
|
|
4163
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4109
4164
|
connectionId: connection2.id,
|
|
4110
4165
|
message: probeError == null ? void 0 : probeError.message,
|
|
4111
4166
|
shop: connection2.shop
|
|
4112
4167
|
});
|
|
4113
4168
|
}
|
|
4114
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4169
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4115
4170
|
writes.push({
|
|
4116
4171
|
collection: "connection",
|
|
4117
4172
|
data: {
|
|
@@ -4182,6 +4237,9 @@ var shopify_default2 = {
|
|
|
4182
4237
|
metered: metered ?? null,
|
|
4183
4238
|
meterProbed: metered !== void 0,
|
|
4184
4239
|
pingedAt: /* @__PURE__ */ new Date(),
|
|
4240
|
+
// One entry per boundary charge judged this run: what
|
|
4241
|
+
// Shopify charged vs what the window's orders billed.
|
|
4242
|
+
...reconciliation && { reconciliation },
|
|
4185
4243
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4186
4244
|
refreshTokenRotated,
|
|
4187
4245
|
scopesMissing,
|
package/package.json
CHANGED