@drawbridge/drawbridge-utils 0.0.142 → 0.0.144
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connections/index.cjs +98 -20
- package/dist/connections/index.d.cts +137 -30
- package/dist/connections/index.d.ts +137 -30
- package/dist/connections/index.js +100 -22
- package/dist/providers.cjs +119 -60
- package/dist/providers.d.cts +14 -3
- package/dist/providers.d.ts +14 -3
- package/dist/providers.js +121 -62
- package/package.json +1 -1
|
@@ -3366,6 +3366,26 @@ var toCanonicalEmail = (value) => {
|
|
|
3366
3366
|
return local + "@" + domain;
|
|
3367
3367
|
};
|
|
3368
3368
|
|
|
3369
|
+
// lib/encrypt.js
|
|
3370
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
3371
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3372
|
+
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
3373
|
+
var decrypt = (value) => {
|
|
3374
|
+
if (typeof value !== "string") return value;
|
|
3375
|
+
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
3376
|
+
const decipher = import_crypto.default.createDecipheriv(
|
|
3377
|
+
ALGORITHM,
|
|
3378
|
+
getKey(),
|
|
3379
|
+
Buffer.from(ivHex, "hex")
|
|
3380
|
+
);
|
|
3381
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
3382
|
+
const result = Buffer.concat([
|
|
3383
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
3384
|
+
decipher.final()
|
|
3385
|
+
]);
|
|
3386
|
+
return JSON.parse(result.toString("utf8"));
|
|
3387
|
+
};
|
|
3388
|
+
|
|
3369
3389
|
// lib/slugify.js
|
|
3370
3390
|
var import_slugify = __toESM(require("slugify"), 1);
|
|
3371
3391
|
var slugify = (value, nochars = false) => {
|
|
@@ -3420,6 +3440,8 @@ var generateDiscountCode = (0, import_nanoid2.customAlphabet)("0123456789ABCDEFG
|
|
|
3420
3440
|
var ORDER_EVENT_HANDLE = slugify("drawbridge-orders");
|
|
3421
3441
|
var REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
3422
3442
|
var OAUTH_ERROR_SOURCE = "oauth";
|
|
3443
|
+
var BILLING_ERROR_SOURCE = "billing";
|
|
3444
|
+
var BILLING_ERROR_MESSAGE = "Order billing isn't set up for this store \u2014 reselect and approve the plan from the Shopify connection page.";
|
|
3423
3445
|
var OAUTH_GRANT_REVOKED_CODES = ["application_cannot_be_found", "invalid_grant"];
|
|
3424
3446
|
var inbound = {
|
|
3425
3447
|
headers: {
|
|
@@ -3940,6 +3962,9 @@ var shopify_default2 = {
|
|
|
3940
3962
|
}
|
|
3941
3963
|
}
|
|
3942
3964
|
const billable = (org == null ? void 0 : org.billingProvider) === "shopify" && fee > 0 && !backfill;
|
|
3965
|
+
const providerRow = billable ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
3966
|
+
const { orderEventHandle } = (providerRow == null ? void 0 : providerRow.settings) ? decrypt(providerRow.settings) : {};
|
|
3967
|
+
const handle = typeof orderEventHandle === "string" && slugify(orderEventHandle) || ORDER_EVENT_HANDLE;
|
|
3943
3968
|
if (billable && !((_f = connection2 == null ? void 0 : connection2.source) == null ? void 0 : _f.id)) {
|
|
3944
3969
|
(_g = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _g.call(logger2, new Error(
|
|
3945
3970
|
"shopify.usage.billing.skipped on order " + orderId + ": " + Math.round(fee * 100) + " cents not billed \u2014 connection " + (connection2 == null ? void 0 : connection2.id) + " has no source.id"
|
|
@@ -3958,7 +3983,7 @@ var shopify_default2 = {
|
|
|
3958
3983
|
// verify it against the transaction prefix and refuse a
|
|
3959
3984
|
// drifted pair — idempotency keys are permanent, so a
|
|
3960
3985
|
// mistraceable event can never be resent under its own key.
|
|
3961
|
-
handle
|
|
3986
|
+
handle,
|
|
3962
3987
|
idempotencyKey: String(orderId),
|
|
3963
3988
|
orderDocId,
|
|
3964
3989
|
orderId: String(orderId),
|
|
@@ -3968,7 +3993,7 @@ var shopify_default2 = {
|
|
|
3968
3993
|
// here — the event handle plus the order id — and sent as the
|
|
3969
3994
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
3970
3995
|
// the order as billed.transaction.
|
|
3971
|
-
transaction:
|
|
3996
|
+
transaction: handle + "." + orderId,
|
|
3972
3997
|
value: Math.round(fee * 100)
|
|
3973
3998
|
},
|
|
3974
3999
|
name: "billing",
|
|
@@ -4112,7 +4137,7 @@ var shopify_default2 = {
|
|
|
4112
4137
|
// against the ones the app now needs, and queues a webhook
|
|
4113
4138
|
// reconciliation.
|
|
4114
4139
|
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
4115
|
-
var _a, _b;
|
|
4140
|
+
var _a, _b, _c;
|
|
4116
4141
|
const request2 = {
|
|
4117
4142
|
connectionId: workflow.connection,
|
|
4118
4143
|
organizationId: workflow.organization,
|
|
@@ -4134,7 +4159,7 @@ var shopify_default2 = {
|
|
|
4134
4159
|
const writes = [];
|
|
4135
4160
|
let source = connection2.source || null;
|
|
4136
4161
|
let sourceRepaired = false;
|
|
4137
|
-
let metered
|
|
4162
|
+
let metered;
|
|
4138
4163
|
let shopCurrency = null;
|
|
4139
4164
|
let billingProbeFailed = false;
|
|
4140
4165
|
try {
|
|
@@ -4153,25 +4178,65 @@ var shopify_default2 = {
|
|
|
4153
4178
|
domain: connection2.shop
|
|
4154
4179
|
});
|
|
4155
4180
|
if (subscriptions.length && subscriptions.every((subscription) => typeof subscription.metered === "boolean")) {
|
|
4156
|
-
metered = subscriptions.
|
|
4181
|
+
metered = ((_a = subscriptions.find((subscription) => subscription.usageLineItemId)) == null ? void 0 : _a.usageLineItemId) || null;
|
|
4157
4182
|
}
|
|
4158
4183
|
} catch (probeError) {
|
|
4159
4184
|
billingProbeFailed = true;
|
|
4160
|
-
(
|
|
4185
|
+
(_b = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _b.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4161
4186
|
connectionId: connection2.id,
|
|
4162
4187
|
message: probeError == null ? void 0 : probeError.message,
|
|
4163
4188
|
shop: connection2.shop
|
|
4164
4189
|
});
|
|
4165
4190
|
}
|
|
4166
|
-
if (sourceRepaired || metered !==
|
|
4191
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_c = connection2.source) == null ? void 0 : _c.metered) ?? void 0)) {
|
|
4192
|
+
writes.push({
|
|
4193
|
+
collection: "connection",
|
|
4194
|
+
data: {
|
|
4195
|
+
$set: sourceRepaired ? { source: { ...source, ...metered !== void 0 && { metered } } } : { "source.metered": metered }
|
|
4196
|
+
},
|
|
4197
|
+
operation: "update",
|
|
4198
|
+
query: { id: connection2.id }
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
4201
|
+
const hasBillingIssue = (connection2.errors || []).some((entry) => entry.source === BILLING_ERROR_SOURCE);
|
|
4202
|
+
if (metered === null && !hasBillingIssue) {
|
|
4203
|
+
writes.push({
|
|
4204
|
+
collection: "connection",
|
|
4205
|
+
data: {
|
|
4206
|
+
$push: {
|
|
4207
|
+
errors: {
|
|
4208
|
+
message: BILLING_ERROR_MESSAGE,
|
|
4209
|
+
source: BILLING_ERROR_SOURCE
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
},
|
|
4213
|
+
operation: "update",
|
|
4214
|
+
query: { id: connection2.id }
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
if (typeof metered === "string" && hasBillingIssue) {
|
|
4167
4218
|
writes.push({
|
|
4168
4219
|
collection: "connection",
|
|
4169
4220
|
data: {
|
|
4170
|
-
$
|
|
4221
|
+
$pull: {
|
|
4222
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4223
|
+
}
|
|
4171
4224
|
},
|
|
4172
4225
|
operation: "update",
|
|
4173
4226
|
query: { id: connection2.id }
|
|
4174
4227
|
});
|
|
4228
|
+
writes.push({
|
|
4229
|
+
collection: "connection",
|
|
4230
|
+
data: {
|
|
4231
|
+
$set: { status: "active" }
|
|
4232
|
+
},
|
|
4233
|
+
operation: "update",
|
|
4234
|
+
query: {
|
|
4235
|
+
id: connection2.id,
|
|
4236
|
+
status: "error",
|
|
4237
|
+
"errors.0": { $exists: false }
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4175
4240
|
}
|
|
4176
4241
|
return {
|
|
4177
4242
|
writes,
|
|
@@ -4187,11 +4252,12 @@ var shopify_default2 = {
|
|
|
4187
4252
|
options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto5.randomUUID)() },
|
|
4188
4253
|
queue: "connection"
|
|
4189
4254
|
}],
|
|
4190
|
-
message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered ===
|
|
4255
|
+
message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
|
|
4191
4256
|
request: request2,
|
|
4192
4257
|
response: {
|
|
4193
4258
|
billingProbeFailed,
|
|
4194
|
-
metered,
|
|
4259
|
+
metered: metered ?? null,
|
|
4260
|
+
meterProbed: metered !== void 0,
|
|
4195
4261
|
pingedAt: /* @__PURE__ */ new Date(),
|
|
4196
4262
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4197
4263
|
refreshTokenRotated,
|
|
@@ -4336,7 +4402,15 @@ var shopify_default2 = {
|
|
|
4336
4402
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4337
4403
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4338
4404
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4339
|
-
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
4405
|
+
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true },
|
|
4406
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
4407
|
+
// changes: stored value overrides the built-in default
|
|
4408
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
4409
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
4410
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
4411
|
+
// means the default, and the sender still refuses any handle that
|
|
4412
|
+
// disagrees with the transaction it minted.
|
|
4413
|
+
{ format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", required: false, setting: true }
|
|
4340
4414
|
]
|
|
4341
4415
|
},
|
|
4342
4416
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4478,20 +4552,24 @@ var shopify_default2 = {
|
|
|
4478
4552
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4479
4553
|
{
|
|
4480
4554
|
action: "billing",
|
|
4481
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4482
|
-
title: "Approve your plan"
|
|
4555
|
+
message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
|
|
4556
|
+
title: "Approve your plan",
|
|
4557
|
+
type: "warning"
|
|
4483
4558
|
}
|
|
4484
4559
|
] : [],
|
|
4485
4560
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
4486
|
-
// the daily health check and the approval webhook (`source.metered
|
|
4487
|
-
//
|
|
4488
|
-
//
|
|
4489
|
-
//
|
|
4490
|
-
|
|
4561
|
+
// the daily health check and the approval webhook (`source.metered`:
|
|
4562
|
+
// the usage line's id when present, null when probed-and-absent).
|
|
4563
|
+
// Approved pricing is never retroactive, so the fix is a fresh
|
|
4564
|
+
// approval. Gated on an EXPLICIT null — a connection the probe hasn't
|
|
4565
|
+
// reached (or couldn't read) has no key and shows nothing rather than
|
|
4566
|
+
// nagging on silence.
|
|
4567
|
+
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4491
4568
|
{
|
|
4492
4569
|
action: "billing",
|
|
4493
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4494
|
-
title: "Fix order billing"
|
|
4570
|
+
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged. You'll need to reselect your plan on Shopify and approve it to fix this.",
|
|
4571
|
+
title: "Fix order billing",
|
|
4572
|
+
type: "error"
|
|
4495
4573
|
}
|
|
4496
4574
|
] : []
|
|
4497
4575
|
];
|
|
@@ -7,6 +7,7 @@ import { channels } from '../pricing.cjs';
|
|
|
7
7
|
import { customAlphabet } from 'nanoid';
|
|
8
8
|
import { toCanonicalEmail } from '../email.cjs';
|
|
9
9
|
import { conversionRate } from '../plans.cjs';
|
|
10
|
+
import { decrypt } from '../encrypt.cjs';
|
|
10
11
|
import { slugify } from '../slugify.cjs';
|
|
11
12
|
import { safeRequest } from '../safe-http.cjs';
|
|
12
13
|
import 'libphonenumber-js';
|
|
@@ -19,6 +20,8 @@ import '../index.cjs';
|
|
|
19
20
|
import 'currency-codes';
|
|
20
21
|
import '../color.cjs';
|
|
21
22
|
import 'tinycolor2';
|
|
23
|
+
import 'crypto';
|
|
24
|
+
import '../token.cjs';
|
|
22
25
|
import 'slugify';
|
|
23
26
|
import 'dns';
|
|
24
27
|
import 'node:http';
|
|
@@ -3869,6 +3872,13 @@ const REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1000;
|
|
|
3869
3872
|
|
|
3870
3873
|
const OAUTH_ERROR_SOURCE = 'oauth';
|
|
3871
3874
|
|
|
3875
|
+
// The billing Issue a probed-unmetered store carries in its connection
|
|
3876
|
+
// errors, written and cleared beside the source.metered stamp (health check
|
|
3877
|
+
// and drawbridge-sync's approval restamp share the source tag). One string,
|
|
3878
|
+
// used by both writers, so the entries dedupe and $pull cleanly.
|
|
3879
|
+
const BILLING_ERROR_SOURCE = 'billing';
|
|
3880
|
+
const BILLING_ERROR_MESSAGE = 'Order billing isn\'t set up for this store — reselect and approve the plan from the Shopify connection page.';
|
|
3881
|
+
|
|
3872
3882
|
// What Shopify says when the merchant has uninstalled or revoked. Neither is
|
|
3873
3883
|
// retryable and both mean the same thing to a merchant: reconnect.
|
|
3874
3884
|
const OAUTH_GRANT_REVOKED_CODES = [ 'application_cannot_be_found', 'invalid_grant' ];
|
|
@@ -4592,6 +4602,22 @@ var shopify = {
|
|
|
4592
4602
|
// roll back.
|
|
4593
4603
|
const billable = org?.billingProvider === 'shopify' && fee > 0 && ! backfill;
|
|
4594
4604
|
|
|
4605
|
+
// THE EFFECTIVE METER HANDLE. The provider row's orderEventHandle
|
|
4606
|
+
// (admin-editable, for the day the plan config's meter changes)
|
|
4607
|
+
// overrides the built-in default; slugified again on read as belt
|
|
4608
|
+
// and braces with the save-side formatter, since a drifted stored
|
|
4609
|
+
// value would mint transactions no meter classifies. Read directly
|
|
4610
|
+
// (not via providerSettings — that module imports this one) and
|
|
4611
|
+
// only for a billable order, which keeps the cost off every
|
|
4612
|
+
// ordinary redemption.
|
|
4613
|
+
const providerRow = billable
|
|
4614
|
+
? await read.get({ collection : 'provider', query : { slug : 'shopify' } })
|
|
4615
|
+
: null;
|
|
4616
|
+
|
|
4617
|
+
const { orderEventHandle } = providerRow?.settings ? decrypt( providerRow.settings ) : {};
|
|
4618
|
+
|
|
4619
|
+
const handle = ( typeof orderEventHandle === 'string' && slugify( orderEventHandle ) ) || ORDER_EVENT_HANDLE;
|
|
4620
|
+
|
|
4595
4621
|
// A billable order with no shop id is revenue lost SILENTLY — the
|
|
4596
4622
|
// gate below just yields no job, the order records normally, and the
|
|
4597
4623
|
// connection looks healthy. An Error (not a warn) because this is
|
|
@@ -4622,7 +4648,7 @@ var shopify = {
|
|
|
4622
4648
|
// verify it against the transaction prefix and refuse a
|
|
4623
4649
|
// drifted pair — idempotency keys are permanent, so a
|
|
4624
4650
|
// mistraceable event can never be resent under its own key.
|
|
4625
|
-
handle
|
|
4651
|
+
handle,
|
|
4626
4652
|
idempotencyKey : String( orderId ),
|
|
4627
4653
|
orderDocId,
|
|
4628
4654
|
orderId : String( orderId ),
|
|
@@ -4632,7 +4658,7 @@ var shopify = {
|
|
|
4632
4658
|
// here — the event handle plus the order id — and sent as the
|
|
4633
4659
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
4634
4660
|
// the order as billed.transaction.
|
|
4635
|
-
transaction :
|
|
4661
|
+
transaction : handle + '.' + orderId,
|
|
4636
4662
|
value : Math.round( fee * 100 )
|
|
4637
4663
|
},
|
|
4638
4664
|
name : 'billing',
|
|
@@ -4866,14 +4892,18 @@ var shopify = {
|
|
|
4866
4892
|
// holds a live admin token, so a missing source is one lookup away
|
|
4867
4893
|
// from fixed.
|
|
4868
4894
|
//
|
|
4869
|
-
// `metered` rides on the same pass:
|
|
4870
|
-
//
|
|
4871
|
-
//
|
|
4872
|
-
//
|
|
4873
|
-
//
|
|
4874
|
-
//
|
|
4875
|
-
//
|
|
4876
|
-
//
|
|
4895
|
+
// `metered` rides on the same pass: the usage LINE ITEM ID from the
|
|
4896
|
+
// merchant's ACTIVE approval — an id is proof of WHICH approval
|
|
4897
|
+
// carries the meter, where a boolean was only an assertion. Three
|
|
4898
|
+
// states: a gid string (probed, usage line present), null (probed
|
|
4899
|
+
// and absent — the unbillable state tasks() nags on), or no key at
|
|
4900
|
+
// all (never probed — silence, never treated as evidence). A meter
|
|
4901
|
+
// added to the app's pricing config is never retroactive — it
|
|
4902
|
+
// bills only from the moment the merchant approves the plan
|
|
4903
|
+
// version that carries it — so a null here means every order's
|
|
4904
|
+
// usage event is 202-accepted and silently never billed. Probed
|
|
4905
|
+
// every run, so a re-approval both clears the card within a day
|
|
4906
|
+
// and refreshes the id it re-minted.
|
|
4877
4907
|
//
|
|
4878
4908
|
// Best-effort by design: the ping above already proved access, so a
|
|
4879
4909
|
// blip in either lookup must not error a healthy connection — the
|
|
@@ -4886,7 +4916,9 @@ var shopify = {
|
|
|
4886
4916
|
|
|
4887
4917
|
let source = connection.source || null;
|
|
4888
4918
|
let sourceRepaired = false;
|
|
4889
|
-
|
|
4919
|
+
// undefined = no verdict this run; a string/null verdict comes
|
|
4920
|
+
// only from a successful probe of an ACTIVE approval.
|
|
4921
|
+
let metered;
|
|
4890
4922
|
let shopCurrency = null;
|
|
4891
4923
|
let billingProbeFailed = false;
|
|
4892
4924
|
|
|
@@ -4913,14 +4945,16 @@ var shopify = {
|
|
|
4913
4945
|
|
|
4914
4946
|
// Only an ACTIVE approval says anything about metering — no
|
|
4915
4947
|
// subscriptions (mid-install, plan pending) leaves the stamp
|
|
4916
|
-
// untouched rather than asserting
|
|
4948
|
+
// untouched rather than asserting null from silence. The
|
|
4917
4949
|
// every-boolean guard is version skew insurance: an older
|
|
4918
|
-
// @drawbridge/shopify returns subscriptions without the flag
|
|
4919
|
-
// and reading that silence as
|
|
4920
|
-
// merchant to re-approve a
|
|
4950
|
+
// @drawbridge/shopify returns subscriptions without the flag
|
|
4951
|
+
// (and without usageLineItemId), and reading that silence as
|
|
4952
|
+
// unmetered would nag every healthy merchant to re-approve a
|
|
4953
|
+
// plan that is fine — the flag being boolean proves an SDK
|
|
4954
|
+
// that also reports the line id.
|
|
4921
4955
|
if( subscriptions.length && subscriptions.every( ( subscription ) => typeof subscription.metered === 'boolean' ) ){
|
|
4922
4956
|
|
|
4923
|
-
metered = subscriptions.
|
|
4957
|
+
metered = subscriptions.find( ( subscription ) => subscription.usageLineItemId )?.usageLineItemId || null;
|
|
4924
4958
|
|
|
4925
4959
|
}
|
|
4926
4960
|
|
|
@@ -4936,13 +4970,16 @@ var shopify = {
|
|
|
4936
4970
|
|
|
4937
4971
|
}
|
|
4938
4972
|
|
|
4939
|
-
|
|
4973
|
+
// A verdict writes only when it CHANGES the stored one — the daily
|
|
4974
|
+
// run stays idempotent, and undefined (no verdict) never touches
|
|
4975
|
+
// the stamp.
|
|
4976
|
+
if( sourceRepaired || ( metered !== undefined && metered !== ( connection.source?.metered ?? undefined ) ) ){
|
|
4940
4977
|
|
|
4941
4978
|
writes.push({
|
|
4942
4979
|
collection : 'connection',
|
|
4943
4980
|
data : {
|
|
4944
4981
|
$set : sourceRepaired
|
|
4945
|
-
? { source : { ...source, ...( metered !==
|
|
4982
|
+
? { source : { ...source, ...( metered !== undefined && { metered }) } }
|
|
4946
4983
|
: { 'source.metered' : metered }
|
|
4947
4984
|
},
|
|
4948
4985
|
operation : 'update',
|
|
@@ -4951,6 +4988,63 @@ var shopify = {
|
|
|
4951
4988
|
|
|
4952
4989
|
}
|
|
4953
4990
|
|
|
4991
|
+
// THE ISSUE MARKER rides beside the stamp: a probed-unmetered
|
|
4992
|
+
// store carries a billing entry in the connection's own errors —
|
|
4993
|
+
// the Issues surface a merchant already knows — and a metered
|
|
4994
|
+
// verdict clears it. Keyed on PRESENCE rather than the verdict
|
|
4995
|
+
// changing, so a connection stamped before this writer existed
|
|
4996
|
+
// still gains its entry on the next run. Status is deliberately
|
|
4997
|
+
// untouched on the way in (orders must keep recording), and the
|
|
4998
|
+
// way out mirrors the resume idiom: reactivate only a connection
|
|
4999
|
+
// that is errored with nothing else wrong.
|
|
5000
|
+
const hasBillingIssue = ( connection.errors || [] ).some( ( entry ) => entry.source === BILLING_ERROR_SOURCE );
|
|
5001
|
+
|
|
5002
|
+
if( metered === null && ! hasBillingIssue ){
|
|
5003
|
+
|
|
5004
|
+
writes.push({
|
|
5005
|
+
collection : 'connection',
|
|
5006
|
+
data : {
|
|
5007
|
+
$push : {
|
|
5008
|
+
errors : {
|
|
5009
|
+
message : BILLING_ERROR_MESSAGE,
|
|
5010
|
+
source : BILLING_ERROR_SOURCE
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
},
|
|
5014
|
+
operation : 'update',
|
|
5015
|
+
query : { id : connection.id }
|
|
5016
|
+
});
|
|
5017
|
+
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
if( typeof metered === 'string' && hasBillingIssue ){
|
|
5021
|
+
|
|
5022
|
+
writes.push({
|
|
5023
|
+
collection : 'connection',
|
|
5024
|
+
data : {
|
|
5025
|
+
$pull : {
|
|
5026
|
+
errors : { source : BILLING_ERROR_SOURCE }
|
|
5027
|
+
}
|
|
5028
|
+
},
|
|
5029
|
+
operation : 'update',
|
|
5030
|
+
query : { id : connection.id }
|
|
5031
|
+
});
|
|
5032
|
+
|
|
5033
|
+
writes.push({
|
|
5034
|
+
collection : 'connection',
|
|
5035
|
+
data : {
|
|
5036
|
+
$set : { status : 'active' }
|
|
5037
|
+
},
|
|
5038
|
+
operation : 'update',
|
|
5039
|
+
query : {
|
|
5040
|
+
id : connection.id,
|
|
5041
|
+
status : 'error',
|
|
5042
|
+
'errors.0' : { $exists : false }
|
|
5043
|
+
}
|
|
5044
|
+
});
|
|
5045
|
+
|
|
5046
|
+
}
|
|
5047
|
+
|
|
4954
5048
|
return {
|
|
4955
5049
|
writes,
|
|
4956
5050
|
enqueues : [ {
|
|
@@ -4972,11 +5066,12 @@ var shopify = {
|
|
|
4972
5066
|
: 'Health check passed — ping ok, webhooks reconciled.'
|
|
4973
5067
|
)
|
|
4974
5068
|
+ ( sourceRepaired ? ' Billing source repaired from the live shop.' : '' )
|
|
4975
|
-
+ ( metered ===
|
|
5069
|
+
+ ( metered === null ? ' No usage line on the store\'s approved plan — order billing needs the merchant to approve the updated plan.' : '' ),
|
|
4976
5070
|
request,
|
|
4977
5071
|
response : {
|
|
4978
5072
|
billingProbeFailed,
|
|
4979
|
-
metered,
|
|
5073
|
+
metered : metered ?? null,
|
|
5074
|
+
meterProbed : metered !== undefined,
|
|
4980
5075
|
pingedAt : new Date(),
|
|
4981
5076
|
refreshTokenExpiresAt : refreshTokenExpiresAt || null,
|
|
4982
5077
|
refreshTokenRotated,
|
|
@@ -5162,7 +5257,15 @@ var shopify = {
|
|
|
5162
5257
|
{ input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', required : true },
|
|
5163
5258
|
{ input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
|
|
5164
5259
|
{ input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
|
|
5165
|
-
{ input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true }
|
|
5260
|
+
{ input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true },
|
|
5261
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
5262
|
+
// changes: stored value overrides the built-in default
|
|
5263
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
5264
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
5265
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
5266
|
+
// means the default, and the sender still refuses any handle that
|
|
5267
|
+
// disagrees with the transaction it minted.
|
|
5268
|
+
{ format : 'slug', input : 'text', key : 'orderEventHandle', label : 'Order event handle', required : false, setting : true }
|
|
5166
5269
|
]
|
|
5167
5270
|
},
|
|
5168
5271
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -5314,23 +5417,27 @@ var shopify = {
|
|
|
5314
5417
|
? [
|
|
5315
5418
|
{
|
|
5316
5419
|
action : 'billing',
|
|
5317
|
-
message : 'Approve the Drawbridge plan to activate this connection. You\'ll
|
|
5318
|
-
title : 'Approve your plan'
|
|
5420
|
+
message : 'Approve the Drawbridge plan to activate this connection. You\'ll be asked to select a plan on Shopify, then everything else happens here.',
|
|
5421
|
+
title : 'Approve your plan',
|
|
5422
|
+
type : 'warning'
|
|
5319
5423
|
}
|
|
5320
5424
|
]
|
|
5321
5425
|
: []
|
|
5322
5426
|
),
|
|
5323
5427
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
5324
|
-
// the daily health check and the approval webhook (`source.metered
|
|
5325
|
-
//
|
|
5326
|
-
//
|
|
5327
|
-
//
|
|
5328
|
-
|
|
5428
|
+
// the daily health check and the approval webhook (`source.metered`:
|
|
5429
|
+
// the usage line's id when present, null when probed-and-absent).
|
|
5430
|
+
// Approved pricing is never retroactive, so the fix is a fresh
|
|
5431
|
+
// approval. Gated on an EXPLICIT null — a connection the probe hasn't
|
|
5432
|
+
// reached (or couldn't read) has no key and shows nothing rather than
|
|
5433
|
+
// nagging on silence.
|
|
5434
|
+
...( data?.status === 'active' && data?.source?.metered === null
|
|
5329
5435
|
? [
|
|
5330
5436
|
{
|
|
5331
5437
|
action : 'billing',
|
|
5332
|
-
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged.
|
|
5333
|
-
title : 'Fix order billing'
|
|
5438
|
+
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged. You\'ll need to reselect your plan on Shopify and approve it to fix this.',
|
|
5439
|
+
title : 'Fix order billing',
|
|
5440
|
+
type : 'error'
|
|
5334
5441
|
}
|
|
5335
5442
|
]
|
|
5336
5443
|
: []
|