@drawbridge/drawbridge-utils 0.0.143 → 0.0.145
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 +99 -10
- package/dist/connections/index.d.cts +118 -10
- package/dist/connections/index.d.ts +118 -10
- package/dist/connections/index.js +101 -12
- package/dist/providers.cjs +120 -50
- package/dist/providers.d.cts +14 -3
- package/dist/providers.d.ts +14 -3
- package/dist/providers.js +122 -52
- 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",
|
|
@@ -4173,6 +4198,46 @@ var shopify_default2 = {
|
|
|
4173
4198
|
query: { id: connection2.id }
|
|
4174
4199
|
});
|
|
4175
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) {
|
|
4218
|
+
writes.push({
|
|
4219
|
+
collection: "connection",
|
|
4220
|
+
data: {
|
|
4221
|
+
$pull: {
|
|
4222
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4223
|
+
}
|
|
4224
|
+
},
|
|
4225
|
+
operation: "update",
|
|
4226
|
+
query: { id: connection2.id }
|
|
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
|
+
});
|
|
4240
|
+
}
|
|
4176
4241
|
return {
|
|
4177
4242
|
writes,
|
|
4178
4243
|
enqueues: [{
|
|
@@ -4337,7 +4402,15 @@ var shopify_default2 = {
|
|
|
4337
4402
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4338
4403
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4339
4404
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4340
|
-
{ 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 }
|
|
4341
4414
|
]
|
|
4342
4415
|
},
|
|
4343
4416
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4355,9 +4428,18 @@ var shopify_default2 = {
|
|
|
4355
4428
|
// there is nothing further to choose. `shop` absent means the install did not
|
|
4356
4429
|
// finish, which is a credential problem rather than a setup one, so the
|
|
4357
4430
|
// stored status already says so.
|
|
4358
|
-
//
|
|
4359
|
-
//
|
|
4360
|
-
|
|
4431
|
+
//
|
|
4432
|
+
// ONE presentation downgrade: an ACTIVE store whose approval carries no
|
|
4433
|
+
// usage meter (source.metered explicitly null) PRESENTS as error, so the
|
|
4434
|
+
// connections list shows the red state and the merchant knows to click in
|
|
4435
|
+
// — where the Fix-order-billing card and the billing Issue explain the
|
|
4436
|
+
// click. The STORED status stays active on purpose: serving and tracking
|
|
4437
|
+
// gate on it, and the orders recorded while the merchant fixes the plan
|
|
4438
|
+
// are the ones billed afterward.
|
|
4439
|
+
status: (data2) => {
|
|
4440
|
+
var _a;
|
|
4441
|
+
return (data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? "error" : data2 == null ? void 0 : data2.status;
|
|
4442
|
+
},
|
|
4361
4443
|
// Step types name the CAPABILITY, not this vendor. A second store platform
|
|
4362
4444
|
// implements the same four commerce steps, and the connection on the step
|
|
4363
4445
|
// says which store it runs against — so a merchant sees one "Create
|
|
@@ -4479,8 +4561,9 @@ var shopify_default2 = {
|
|
|
4479
4561
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4480
4562
|
{
|
|
4481
4563
|
action: "billing",
|
|
4482
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4483
|
-
title: "Approve your plan"
|
|
4564
|
+
message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
|
|
4565
|
+
title: "Approve your plan",
|
|
4566
|
+
type: "warning"
|
|
4484
4567
|
}
|
|
4485
4568
|
] : [],
|
|
4486
4569
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
@@ -4493,8 +4576,9 @@ var shopify_default2 = {
|
|
|
4493
4576
|
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4494
4577
|
{
|
|
4495
4578
|
action: "billing",
|
|
4496
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4497
|
-
title: "Fix order billing"
|
|
4579
|
+
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.",
|
|
4580
|
+
title: "Fix order billing",
|
|
4581
|
+
type: "error"
|
|
4498
4582
|
}
|
|
4499
4583
|
] : []
|
|
4500
4584
|
];
|
|
@@ -5100,6 +5184,11 @@ var publicConnectionKeys = Object.freeze([
|
|
|
5100
5184
|
"settings",
|
|
5101
5185
|
"shop",
|
|
5102
5186
|
"slug",
|
|
5187
|
+
// Vendor identity plus the metered billing stamp — domain, id, label,
|
|
5188
|
+
// metered — no credentials live here (those are the encrypted settings
|
|
5189
|
+
// blob). The status and tasks hooks pivot on source.metered, so stripping
|
|
5190
|
+
// it here left the presented status blind to the one state it must show.
|
|
5191
|
+
"source",
|
|
5103
5192
|
"status",
|
|
5104
5193
|
"tasks",
|
|
5105
5194
|
"title",
|
|
@@ -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',
|
|
@@ -4962,6 +4988,63 @@ var shopify = {
|
|
|
4962
4988
|
|
|
4963
4989
|
}
|
|
4964
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
|
+
|
|
4965
5048
|
return {
|
|
4966
5049
|
writes,
|
|
4967
5050
|
enqueues : [ {
|
|
@@ -5174,7 +5257,15 @@ var shopify = {
|
|
|
5174
5257
|
{ input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', required : true },
|
|
5175
5258
|
{ input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
|
|
5176
5259
|
{ input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
|
|
5177
|
-
{ 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 }
|
|
5178
5269
|
]
|
|
5179
5270
|
},
|
|
5180
5271
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -5192,9 +5283,19 @@ var shopify = {
|
|
|
5192
5283
|
// there is nothing further to choose. `shop` absent means the install did not
|
|
5193
5284
|
// finish, which is a credential problem rather than a setup one, so the
|
|
5194
5285
|
// stored status already says so.
|
|
5195
|
-
//
|
|
5196
|
-
//
|
|
5197
|
-
|
|
5286
|
+
//
|
|
5287
|
+
// ONE presentation downgrade: an ACTIVE store whose approval carries no
|
|
5288
|
+
// usage meter (source.metered explicitly null) PRESENTS as error, so the
|
|
5289
|
+
// connections list shows the red state and the merchant knows to click in
|
|
5290
|
+
// — where the Fix-order-billing card and the billing Issue explain the
|
|
5291
|
+
// click. The STORED status stays active on purpose: serving and tracking
|
|
5292
|
+
// gate on it, and the orders recorded while the merchant fixes the plan
|
|
5293
|
+
// are the ones billed afterward.
|
|
5294
|
+
status : ( data ) => (
|
|
5295
|
+
data?.status === 'active' && data?.source?.metered === null
|
|
5296
|
+
? 'error'
|
|
5297
|
+
: data?.status
|
|
5298
|
+
),
|
|
5198
5299
|
// Step types name the CAPABILITY, not this vendor. A second store platform
|
|
5199
5300
|
// implements the same four commerce steps, and the connection on the step
|
|
5200
5301
|
// says which store it runs against — so a merchant sees one "Create
|
|
@@ -5326,8 +5427,9 @@ var shopify = {
|
|
|
5326
5427
|
? [
|
|
5327
5428
|
{
|
|
5328
5429
|
action : 'billing',
|
|
5329
|
-
message : 'Approve the Drawbridge plan to activate this connection. You\'ll
|
|
5330
|
-
title : 'Approve your plan'
|
|
5430
|
+
message : 'Approve the Drawbridge plan to activate this connection. You\'ll be asked to select a plan on Shopify, then everything else happens here.',
|
|
5431
|
+
title : 'Approve your plan',
|
|
5432
|
+
type : 'warning'
|
|
5331
5433
|
}
|
|
5332
5434
|
]
|
|
5333
5435
|
: []
|
|
@@ -5343,8 +5445,9 @@ var shopify = {
|
|
|
5343
5445
|
? [
|
|
5344
5446
|
{
|
|
5345
5447
|
action : 'billing',
|
|
5346
|
-
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged.
|
|
5347
|
-
title : 'Fix order billing'
|
|
5448
|
+
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.',
|
|
5449
|
+
title : 'Fix order billing',
|
|
5450
|
+
type : 'error'
|
|
5348
5451
|
}
|
|
5349
5452
|
]
|
|
5350
5453
|
: []
|
|
@@ -6361,6 +6464,11 @@ const publicConnectionKeys = Object.freeze([
|
|
|
6361
6464
|
'settings',
|
|
6362
6465
|
'shop',
|
|
6363
6466
|
'slug',
|
|
6467
|
+
// Vendor identity plus the metered billing stamp — domain, id, label,
|
|
6468
|
+
// metered — no credentials live here (those are the encrypted settings
|
|
6469
|
+
// blob). The status and tasks hooks pivot on source.metered, so stripping
|
|
6470
|
+
// it here left the presented status blind to the one state it must show.
|
|
6471
|
+
'source',
|
|
6364
6472
|
'status',
|
|
6365
6473
|
'tasks',
|
|
6366
6474
|
'title',
|
|
@@ -7,6 +7,7 @@ import { channels } from '../pricing.js';
|
|
|
7
7
|
import { customAlphabet } from 'nanoid';
|
|
8
8
|
import { toCanonicalEmail } from '../email.js';
|
|
9
9
|
import { conversionRate } from '../plans.js';
|
|
10
|
+
import { decrypt } from '../encrypt.js';
|
|
10
11
|
import { slugify } from '../slugify.js';
|
|
11
12
|
import { safeRequest } from '../safe-http.js';
|
|
12
13
|
import 'libphonenumber-js';
|
|
@@ -19,6 +20,8 @@ import '../index.js';
|
|
|
19
20
|
import 'currency-codes';
|
|
20
21
|
import '../color.js';
|
|
21
22
|
import 'tinycolor2';
|
|
23
|
+
import 'crypto';
|
|
24
|
+
import '../token.js';
|
|
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',
|
|
@@ -4962,6 +4988,63 @@ var shopify = {
|
|
|
4962
4988
|
|
|
4963
4989
|
}
|
|
4964
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
|
+
|
|
4965
5048
|
return {
|
|
4966
5049
|
writes,
|
|
4967
5050
|
enqueues : [ {
|
|
@@ -5174,7 +5257,15 @@ var shopify = {
|
|
|
5174
5257
|
{ input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', required : true },
|
|
5175
5258
|
{ input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
|
|
5176
5259
|
{ input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
|
|
5177
|
-
{ 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 }
|
|
5178
5269
|
]
|
|
5179
5270
|
},
|
|
5180
5271
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -5192,9 +5283,19 @@ var shopify = {
|
|
|
5192
5283
|
// there is nothing further to choose. `shop` absent means the install did not
|
|
5193
5284
|
// finish, which is a credential problem rather than a setup one, so the
|
|
5194
5285
|
// stored status already says so.
|
|
5195
|
-
//
|
|
5196
|
-
//
|
|
5197
|
-
|
|
5286
|
+
//
|
|
5287
|
+
// ONE presentation downgrade: an ACTIVE store whose approval carries no
|
|
5288
|
+
// usage meter (source.metered explicitly null) PRESENTS as error, so the
|
|
5289
|
+
// connections list shows the red state and the merchant knows to click in
|
|
5290
|
+
// — where the Fix-order-billing card and the billing Issue explain the
|
|
5291
|
+
// click. The STORED status stays active on purpose: serving and tracking
|
|
5292
|
+
// gate on it, and the orders recorded while the merchant fixes the plan
|
|
5293
|
+
// are the ones billed afterward.
|
|
5294
|
+
status : ( data ) => (
|
|
5295
|
+
data?.status === 'active' && data?.source?.metered === null
|
|
5296
|
+
? 'error'
|
|
5297
|
+
: data?.status
|
|
5298
|
+
),
|
|
5198
5299
|
// Step types name the CAPABILITY, not this vendor. A second store platform
|
|
5199
5300
|
// implements the same four commerce steps, and the connection on the step
|
|
5200
5301
|
// says which store it runs against — so a merchant sees one "Create
|
|
@@ -5326,8 +5427,9 @@ var shopify = {
|
|
|
5326
5427
|
? [
|
|
5327
5428
|
{
|
|
5328
5429
|
action : 'billing',
|
|
5329
|
-
message : 'Approve the Drawbridge plan to activate this connection. You\'ll
|
|
5330
|
-
title : 'Approve your plan'
|
|
5430
|
+
message : 'Approve the Drawbridge plan to activate this connection. You\'ll be asked to select a plan on Shopify, then everything else happens here.',
|
|
5431
|
+
title : 'Approve your plan',
|
|
5432
|
+
type : 'warning'
|
|
5331
5433
|
}
|
|
5332
5434
|
]
|
|
5333
5435
|
: []
|
|
@@ -5343,8 +5445,9 @@ var shopify = {
|
|
|
5343
5445
|
? [
|
|
5344
5446
|
{
|
|
5345
5447
|
action : 'billing',
|
|
5346
|
-
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged.
|
|
5347
|
-
title : 'Fix order billing'
|
|
5448
|
+
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.',
|
|
5449
|
+
title : 'Fix order billing',
|
|
5450
|
+
type : 'error'
|
|
5348
5451
|
}
|
|
5349
5452
|
]
|
|
5350
5453
|
: []
|
|
@@ -6361,6 +6464,11 @@ const publicConnectionKeys = Object.freeze([
|
|
|
6361
6464
|
'settings',
|
|
6362
6465
|
'shop',
|
|
6363
6466
|
'slug',
|
|
6467
|
+
// Vendor identity plus the metered billing stamp — domain, id, label,
|
|
6468
|
+
// metered — no credentials live here (those are the encrypted settings
|
|
6469
|
+
// blob). The status and tasks hooks pivot on source.metered, so stripping
|
|
6470
|
+
// it here left the presented status blind to the one state it must show.
|
|
6471
|
+
'source',
|
|
6364
6472
|
'status',
|
|
6365
6473
|
'tasks',
|
|
6366
6474
|
'title',
|