@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.
@@ -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: 5,
4198
- occurredAtMin: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString(),
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 accrued = accruals.find((event) => Number(event.usageQuantity) > 0);
4204
- if (accrued) {
4205
- metered = String(accrued.chargeId || accrued.id);
4206
- writes.push({
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
- data: {
4209
- $set: { "billed.receipt": metered }
4210
- },
4211
- // Top-level, not an option — the controller takes
4212
- // `multiple` beside `options`, and the shell
4213
- // forwards it as such.
4214
- multiple: true,
4215
- operation: "update",
4216
- query: {
4217
- "billed.date": { $gte: new Date(accrued.occurredAt || 0) },
4218
- "billed.receipt": { $in: [null] },
4219
- organization: workflow.organization,
4220
- "provider.slug": "shopify"
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
- (_c = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _c.call(logger2, "shopify.health.billing.probe.failed", {
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 !== (((_d = connection2.source) == null ? void 0 : _d.metered) ?? void 0)) {
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). No accrual AND a billed order
4975
- // old enough that one should exist is the HONEST unmetered
4976
- // verdict the null that re-arms the card, the Issue and the
4977
- // pager on truth instead of the Admin API's silence. Off
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 : 5,
4987
- occurredAtMin : new Date( Date.now() - 30 * 24 * 60 * 60 * 1000 ).toISOString(),
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
- const accrued = accruals.find( ( event ) => Number( event.usageQuantity ) > 0 );
4997
-
4998
- if( accrued ){
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
- metered = String( accrued.chargeId || accrued.id );
5117
+ }
5001
5118
 
5002
- // THE BUCKET STAMPS ITS ORDERS. Partner accrual rows
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 negative verdict from an empty ledger: measured
5031
- // 2026-09-04, CHARGE_USAGE rows only materialize at
5032
- // BILLING moments (cycle end, cancellation/replacement)
5033
- // usage accrues invisibly between them, so a
5034
- // first-cycle store with billed orders and no row is
5035
- // the NORMAL state, not an unmetered one. The honest
5036
- // negative is a boundary reconciliation (a charge-out
5037
- // row whose quantity falls short of the window's billed
5038
- // sum) until that exists, absence stays a no-verdict.
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). No accrual AND a billed order
4975
- // old enough that one should exist is the HONEST unmetered
4976
- // verdict the null that re-arms the card, the Issue and the
4977
- // pager on truth instead of the Admin API's silence. Off
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 : 5,
4987
- occurredAtMin : new Date( Date.now() - 30 * 24 * 60 * 60 * 1000 ).toISOString(),
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
- const accrued = accruals.find( ( event ) => Number( event.usageQuantity ) > 0 );
4997
-
4998
- if( accrued ){
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
- metered = String( accrued.chargeId || accrued.id );
5117
+ }
5001
5118
 
5002
- // THE BUCKET STAMPS ITS ORDERS. Partner accrual rows
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 negative verdict from an empty ledger: measured
5031
- // 2026-09-04, CHARGE_USAGE rows only materialize at
5032
- // BILLING moments (cycle end, cancellation/replacement)
5033
- // usage accrues invisibly between them, so a
5034
- // first-cycle store with billed orders and no row is
5035
- // the NORMAL state, not an unmetered one. The honest
5036
- // negative is a boundary reconciliation (a charge-out
5037
- // row whose quantity falls short of the window's billed
5038
- // sum) until that exists, absence stays a no-verdict.
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: 5,
4124
- occurredAtMin: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString(),
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 accrued = accruals.find((event) => Number(event.usageQuantity) > 0);
4130
- if (accrued) {
4131
- metered = String(accrued.chargeId || accrued.id);
4132
- writes.push({
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
- data: {
4135
- $set: { "billed.receipt": metered }
4136
- },
4137
- // Top-level, not an option — the controller takes
4138
- // `multiple` beside `options`, and the shell
4139
- // forwards it as such.
4140
- multiple: true,
4141
- operation: "update",
4142
- query: {
4143
- "billed.date": { $gte: new Date(accrued.occurredAt || 0) },
4144
- "billed.receipt": { $in: [null] },
4145
- organization: workflow.organization,
4146
- "provider.slug": "shopify"
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
- (_c = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _c.call(logger2, "shopify.health.billing.probe.failed", {
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 !== (((_d = connection2.source) == null ? void 0 : _d.metered) ?? void 0)) {
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,
@@ -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: 5,
4123
- occurredAtMin: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString(),
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 accrued = accruals.find((event) => Number(event.usageQuantity) > 0);
4129
- if (accrued) {
4130
- metered = String(accrued.chargeId || accrued.id);
4131
- writes.push({
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
- data: {
4134
- $set: { "billed.receipt": metered }
4135
- },
4136
- // Top-level, not an option — the controller takes
4137
- // `multiple` beside `options`, and the shell
4138
- // forwards it as such.
4139
- multiple: true,
4140
- operation: "update",
4141
- query: {
4142
- "billed.date": { $gte: new Date(accrued.occurredAt || 0) },
4143
- "billed.receipt": { $in: [null] },
4144
- organization: workflow.organization,
4145
- "provider.slug": "shopify"
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
- (_c = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _c.call(logger2, "shopify.health.billing.probe.failed", {
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 !== (((_d = connection2.source) == null ? void 0 : _d.metered) ?? void 0)) {
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: 5,
4079
- occurredAtMin: new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString(),
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 accrued = accruals.find((event) => Number(event.usageQuantity) > 0);
4085
- if (accrued) {
4086
- metered = String(accrued.chargeId || accrued.id);
4087
- writes.push({
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
- data: {
4090
- $set: { "billed.receipt": metered }
4091
- },
4092
- // Top-level, not an option — the controller takes
4093
- // `multiple` beside `options`, and the shell
4094
- // forwards it as such.
4095
- multiple: true,
4096
- operation: "update",
4097
- query: {
4098
- "billed.date": { $gte: new Date(accrued.occurredAt || 0) },
4099
- "billed.receipt": { $in: [null] },
4100
- organization: workflow.organization,
4101
- "provider.slug": "shopify"
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
- (_c = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _c.call(logger2, "shopify.health.billing.probe.failed", {
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 !== (((_d = connection2.source) == null ? void 0 : _d.metered) ?? void 0)) {
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
@@ -216,5 +216,5 @@
216
216
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
217
217
  },
218
218
  "types": "dist/index.d.ts",
219
- "version": "0.0.151"
219
+ "version": "0.0.152"
220
220
  }