@drawbridge/drawbridge-utils 0.0.150 → 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 -17
- package/dist/connections/index.d.cts +143 -26
- package/dist/connections/index.d.ts +143 -26
- package/dist/connections/index.js +82 -17
- package/dist/providers.cjs +82 -17
- package/dist/providers.js +82 -17
- 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,38 +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
|
-
|
|
4207
|
-
|
|
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({
|
|
4208
4223
|
collection: "order",
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
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 });
|
|
4216
4278
|
}
|
|
4217
4279
|
}
|
|
4218
4280
|
} catch (probeError) {
|
|
4219
4281
|
billingProbeFailed = true;
|
|
4220
|
-
(
|
|
4282
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4221
4283
|
connectionId: connection2.id,
|
|
4222
4284
|
message: probeError == null ? void 0 : probeError.message,
|
|
4223
4285
|
shop: connection2.shop
|
|
4224
4286
|
});
|
|
4225
4287
|
}
|
|
4226
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4288
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4227
4289
|
writes.push({
|
|
4228
4290
|
collection: "connection",
|
|
4229
4291
|
data: {
|
|
@@ -4294,6 +4356,9 @@ var shopify_default2 = {
|
|
|
4294
4356
|
metered: metered ?? null,
|
|
4295
4357
|
meterProbed: metered !== void 0,
|
|
4296
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 },
|
|
4297
4362
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4298
4363
|
refreshTokenRotated,
|
|
4299
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,52 +4974,163 @@ 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
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
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({
|
|
5008
5054
|
collection : 'order',
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
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
|
+
});
|
|
5015
5116
|
|
|
5016
|
-
|
|
5117
|
+
}
|
|
5118
|
+
|
|
5119
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt : boundary.occurredAt, orders : orders.length });
|
|
5017
5120
|
|
|
5018
5121
|
}
|
|
5019
5122
|
|
|
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.
|
|
5133
|
+
|
|
5020
5134
|
}
|
|
5021
5135
|
|
|
5022
5136
|
} catch ( probeError ) {
|
|
@@ -5134,6 +5248,9 @@ var shopify = {
|
|
|
5134
5248
|
metered : metered ?? null,
|
|
5135
5249
|
meterProbed : metered !== undefined,
|
|
5136
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 }),
|
|
5137
5254
|
refreshTokenExpiresAt : refreshTokenExpiresAt || null,
|
|
5138
5255
|
refreshTokenRotated,
|
|
5139
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,52 +4974,163 @@ 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
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
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({
|
|
5008
5054
|
collection : 'order',
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
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
|
+
});
|
|
5015
5116
|
|
|
5016
|
-
|
|
5117
|
+
}
|
|
5118
|
+
|
|
5119
|
+
reconciliation.push({ balanced, billed, charge, charged, occurredAt : boundary.occurredAt, orders : orders.length });
|
|
5017
5120
|
|
|
5018
5121
|
}
|
|
5019
5122
|
|
|
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.
|
|
5133
|
+
|
|
5020
5134
|
}
|
|
5021
5135
|
|
|
5022
5136
|
} catch ( probeError ) {
|
|
@@ -5134,6 +5248,9 @@ var shopify = {
|
|
|
5134
5248
|
metered : metered ?? null,
|
|
5135
5249
|
meterProbed : metered !== undefined,
|
|
5136
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 }),
|
|
5137
5254
|
refreshTokenExpiresAt : refreshTokenExpiresAt || null,
|
|
5138
5255
|
refreshTokenRotated,
|
|
5139
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,38 +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
|
-
|
|
4133
|
-
|
|
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({
|
|
4134
4149
|
collection: "order",
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
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 });
|
|
4142
4204
|
}
|
|
4143
4205
|
}
|
|
4144
4206
|
} catch (probeError) {
|
|
4145
4207
|
billingProbeFailed = true;
|
|
4146
|
-
(
|
|
4208
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4147
4209
|
connectionId: connection2.id,
|
|
4148
4210
|
message: probeError == null ? void 0 : probeError.message,
|
|
4149
4211
|
shop: connection2.shop
|
|
4150
4212
|
});
|
|
4151
4213
|
}
|
|
4152
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4214
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4153
4215
|
writes.push({
|
|
4154
4216
|
collection: "connection",
|
|
4155
4217
|
data: {
|
|
@@ -4220,6 +4282,9 @@ var shopify_default2 = {
|
|
|
4220
4282
|
metered: metered ?? null,
|
|
4221
4283
|
meterProbed: metered !== void 0,
|
|
4222
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 },
|
|
4223
4288
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4224
4289
|
refreshTokenRotated,
|
|
4225
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,38 +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
|
-
|
|
4132
|
-
|
|
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({
|
|
4133
4148
|
collection: "order",
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
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 });
|
|
4141
4203
|
}
|
|
4142
4204
|
}
|
|
4143
4205
|
} catch (probeError) {
|
|
4144
4206
|
billingProbeFailed = true;
|
|
4145
|
-
(
|
|
4207
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4146
4208
|
connectionId: connection2.id,
|
|
4147
4209
|
message: probeError == null ? void 0 : probeError.message,
|
|
4148
4210
|
shop: connection2.shop
|
|
4149
4211
|
});
|
|
4150
4212
|
}
|
|
4151
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4213
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4152
4214
|
writes.push({
|
|
4153
4215
|
collection: "connection",
|
|
4154
4216
|
data: {
|
|
@@ -4219,6 +4281,9 @@ var shopify_default2 = {
|
|
|
4219
4281
|
metered: metered ?? null,
|
|
4220
4282
|
meterProbed: metered !== void 0,
|
|
4221
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 },
|
|
4222
4287
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4223
4288
|
refreshTokenRotated,
|
|
4224
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,38 +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
|
-
|
|
4088
|
-
|
|
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({
|
|
4089
4104
|
collection: "order",
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
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 });
|
|
4097
4159
|
}
|
|
4098
4160
|
}
|
|
4099
4161
|
} catch (probeError) {
|
|
4100
4162
|
billingProbeFailed = true;
|
|
4101
|
-
(
|
|
4163
|
+
(_e = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _e.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4102
4164
|
connectionId: connection2.id,
|
|
4103
4165
|
message: probeError == null ? void 0 : probeError.message,
|
|
4104
4166
|
shop: connection2.shop
|
|
4105
4167
|
});
|
|
4106
4168
|
}
|
|
4107
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
4169
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_f = connection2.source) == null ? void 0 : _f.metered) ?? void 0)) {
|
|
4108
4170
|
writes.push({
|
|
4109
4171
|
collection: "connection",
|
|
4110
4172
|
data: {
|
|
@@ -4175,6 +4237,9 @@ var shopify_default2 = {
|
|
|
4175
4237
|
metered: metered ?? null,
|
|
4176
4238
|
meterProbed: metered !== void 0,
|
|
4177
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 },
|
|
4178
4243
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4179
4244
|
refreshTokenRotated,
|
|
4180
4245
|
scopesMissing,
|
package/package.json
CHANGED