@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.
@@ -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: 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
- } else {
4207
- const unaccrued = await read.get({
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
- query: {
4210
- "billed.date": { $lt: new Date(Date.now() - 6 * 60 * 60 * 1e3) },
4211
- organization: workflow.organization,
4212
- "provider.slug": "shopify"
4213
- }
4214
- });
4215
- if (unaccrued) metered = null;
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
- (_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", {
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 !== (((_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)) {
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). 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 ){
4999
-
5000
- metered = String( accrued.chargeId || accrued.id );
5001
-
5002
- } else {
5003
-
5004
- // Only a store that SHOULD have accrued gets the
5005
- // unmetered verdict: an order stamped billed six-plus
5006
- // hours ago with still no accrual event.
5007
- const unaccrued = await read.get({
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
- query : {
5010
- 'billed.date' : { $lt : new Date( Date.now() - 6 * 60 * 60 * 1000 ) },
5011
- organization : workflow.organization,
5012
- 'provider.slug' : 'shopify'
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
- if( unaccrued ) metered = null;
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). 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 ){
4999
-
5000
- metered = String( accrued.chargeId || accrued.id );
5001
-
5002
- } else {
5003
-
5004
- // Only a store that SHOULD have accrued gets the
5005
- // unmetered verdict: an order stamped billed six-plus
5006
- // hours ago with still no accrual event.
5007
- const unaccrued = await read.get({
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
- query : {
5010
- 'billed.date' : { $lt : new Date( Date.now() - 6 * 60 * 60 * 1000 ) },
5011
- organization : workflow.organization,
5012
- 'provider.slug' : 'shopify'
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
- if( unaccrued ) metered = null;
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: 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
- } else {
4133
- const unaccrued = await read.get({
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
- query: {
4136
- "billed.date": { $lt: new Date(Date.now() - 6 * 60 * 60 * 1e3) },
4137
- organization: workflow.organization,
4138
- "provider.slug": "shopify"
4139
- }
4140
- });
4141
- if (unaccrued) metered = null;
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
- (_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", {
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 !== (((_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)) {
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,
@@ -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: 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
- } else {
4132
- const unaccrued = await read.get({
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
- query: {
4135
- "billed.date": { $lt: new Date(Date.now() - 6 * 60 * 60 * 1e3) },
4136
- organization: workflow.organization,
4137
- "provider.slug": "shopify"
4138
- }
4139
- });
4140
- if (unaccrued) metered = null;
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
- (_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", {
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 !== (((_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)) {
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: 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
- } else {
4088
- const unaccrued = await read.get({
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
- query: {
4091
- "billed.date": { $lt: new Date(Date.now() - 6 * 60 * 60 * 1e3) },
4092
- organization: workflow.organization,
4093
- "provider.slug": "shopify"
4094
- }
4095
- });
4096
- if (unaccrued) metered = null;
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
- (_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", {
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 !== (((_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)) {
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
@@ -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.150"
219
+ "version": "0.0.152"
220
220
  }