@drawbridge/drawbridge-agents 0.1.28 → 0.1.31
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.
|
@@ -68,13 +68,20 @@ skill) so it stops living only in tribal memory.
|
|
|
68
68
|
after it produced five distinct defects, ending in a 60-second line-window overlap that billed
|
|
69
69
|
one correctly-delivered event twice. Do not reintroduce meter events, and do not "fix" a billing
|
|
70
70
|
discrepancy by reporting usage to Stripe — corrections are `billing.trueup` ledger rows.
|
|
71
|
-
- **`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`
|
|
71
|
+
- **`subscription.overrides` is the single home for sold/negotiated terms** —
|
|
72
|
+
`{ actions : { limit, overages }, conversion }`. `overrides.actions` is ALWAYS stamped at sale by
|
|
73
|
+
drawbridge-stripe (≥0.1.46) and re-stamped on plan change; `overrides.conversion` exists only when
|
|
74
|
+
an admin overrides the plan default, and api carries it onto the new document across plan changes.
|
|
75
|
+
Resolution is PER FIELD: `overrides.actions.X` → legacy `items.actions.X` (transitional) →
|
|
76
|
+
plans.js; conversion resolves ONLY through `conversionRate()` in utils (`overrides.conversion` →
|
|
77
|
+
legacy top-level `conversion` → plan → free; `??` throughout, 0 is a valid override). `overages`
|
|
78
|
+
is a NUMBER in cents per action — the string `'2.5'` shape exists only at the Stripe
|
|
79
|
+
`unit_amount_decimal` boundary. Deleting a key restores plan flow, which is why the admin PUT
|
|
80
|
+
unsets the legacy keys it supersedes. plans.js is the default for NEW sales only; repricing
|
|
81
|
+
existing customers is `repriceActionsSnapshot` (explicit, dry-runnable — and it overwrites
|
|
82
|
+
admin-negotiated actions values, so read the dry run). The legacy keys are copy-only until the
|
|
83
|
+
overrides cleanup release, and sync (reader) deploys before stripe (writer) on any shape change.
|
|
84
|
+
Detail: drawbridge-docs `reference/billing.md`.
|
|
78
85
|
- **No Stripe product backs actions (≥0.1.45).** Invoice items carry
|
|
79
86
|
currency/quantity/unit_amount_decimal/tax_behavior/description directly; the account keeps exactly
|
|
80
87
|
one product — the plan product behind `STRIPE_PRICE_*`. Never reintroduce a product-presence guard
|
|
@@ -219,6 +226,20 @@ skill) so it stops living only in tribal memory.
|
|
|
219
226
|
- The `write_resource_feedbacks` and `read_publications` scopes live in drawbridge-shopify-app's
|
|
220
227
|
`shopify.app.*.toml` but power other repos: sync's product ResourceFeedback sends and the
|
|
221
228
|
embedded app's publishing section. Removing either → silent Shopify userErrors, no crash.
|
|
229
|
+
- **No Shopify webhook reaches the embedded app.** Every `[[webhooks.subscriptions]]` in
|
|
230
|
+
drawbridge-shopify-app's `shopify.app.*.toml` — `app/uninstalled` included — is delivered to
|
|
231
|
+
drawbridge-webhooks (`route/shopify.js`, topic-agnostic → `buffer` doc), which drawbridge-sync
|
|
232
|
+
consumes (`lib/buffer.js` `shopify.app/uninstalled` → delete connections, hard-delete the `shop`
|
|
233
|
+
record). So `app/routes/webhooks.app.uninstalled.tsx` is unreachable and **nothing in the
|
|
234
|
+
embedded app may hold per-shop state that needs invalidating on uninstall** — no memo, no cache,
|
|
235
|
+
no "already done" set. Violated once: `ensureChannel` cached "this shop is bound to our channel
|
|
236
|
+
spec" in a process-local Set cleared only by that dead route, so uninstall → reinstall skipped
|
|
237
|
+
`channelCreate` and the merchant silently lost the Drawbridge sales channel (2026-08-07 QA).
|
|
238
|
+
- Shopify Admin GraphQL failures come in **two shapes**, and sales-channel code must handle both:
|
|
239
|
+
domain failures land in `userErrors` (HTTP 200), while a revoked/expired offline token,
|
|
240
|
+
throttling, or a field missing on the API version land in a **top-level `errors`** — a bare
|
|
241
|
+
string for a 401, an array of objects otherwise — with a null `data`. Reading only `userErrors`
|
|
242
|
+
turns an auth failure into a silent success.
|
|
222
243
|
- `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/shopify` `lib/constants.js`) must stay in lockstep with
|
|
223
244
|
`[access_scopes]` in drawbridge-shopify-app's `shopify.app.*.toml` (both files). drawbridge-api
|
|
224
245
|
diffs each store's granted scopes against it to flag "update permissions" on the connections
|
package/package.json
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// What communication channels are actually flowing, HubSpot's count against ours.
|
|
2
|
+
//
|
|
3
|
+
// Exists because "the integration is connected" and "the data is arriving" are
|
|
4
|
+
// different claims, and the second is the only one that matters here. HubSpot's
|
|
5
|
+
// communication object holds SMS, WhatsApp and LinkedIn messages, told apart by
|
|
6
|
+
// channel — so whatever writes LinkedIn messages into that object, this pipeline
|
|
7
|
+
// reads. The question is never whether we can read them; it is whether anything is
|
|
8
|
+
// writing them.
|
|
9
|
+
//
|
|
10
|
+
// Same shape as the `missing` figure on the contact ingest, which is what caught 50
|
|
11
|
+
// contacts that had been silently absent for the life of the project.
|
|
12
|
+
|
|
13
|
+
const { growth } = require( '../lib/mongodb' );
|
|
14
|
+
const { enabled, request } = require( '../lib/hubspot' );
|
|
15
|
+
const { run } = require( '../lib/cycle' );
|
|
16
|
+
|
|
17
|
+
// The channels HubSpot files under the communication object. PHYSICAL_MAIL is
|
|
18
|
+
// hidden on this portal but counted anyway, because a hidden property still holds
|
|
19
|
+
// records.
|
|
20
|
+
const CHANNELS = [
|
|
21
|
+
'LINKEDIN_MESSAGE',
|
|
22
|
+
'SMS',
|
|
23
|
+
'WHATS_APP',
|
|
24
|
+
'PHYSICAL_MAIL',
|
|
25
|
+
'CUSTOM_CHANNEL_CONVERSATION'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
run( 'channels', async () => {
|
|
29
|
+
|
|
30
|
+
if( ! enabled() ) return { skipped : 'HUBSPOT_ACCESS_TOKEN not set' };
|
|
31
|
+
|
|
32
|
+
const { db, database } = await growth();
|
|
33
|
+
|
|
34
|
+
const report = {};
|
|
35
|
+
|
|
36
|
+
for( const channel of CHANNELS ){
|
|
37
|
+
|
|
38
|
+
const body = await request({
|
|
39
|
+
body : {
|
|
40
|
+
filterGroups : [
|
|
41
|
+
{
|
|
42
|
+
filters : [
|
|
43
|
+
{
|
|
44
|
+
operator : 'EQ',
|
|
45
|
+
propertyName : 'hs_communication_channel_type',
|
|
46
|
+
value : channel
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
limit : 1
|
|
52
|
+
},
|
|
53
|
+
method : 'POST',
|
|
54
|
+
path : '/crm/v3/objects/communications/search'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const inHubspot = Number( body?.total || 0 );
|
|
58
|
+
|
|
59
|
+
// Ours is counted on the touch, which is one row per contact on the message,
|
|
60
|
+
// so it can legitimately exceed HubSpot's count of messages.
|
|
61
|
+
const held = await database.collection( 'touch' ).countDocuments({
|
|
62
|
+
channel,
|
|
63
|
+
type : 'communication'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
report[ channel ] = {
|
|
67
|
+
held,
|
|
68
|
+
inHubspot,
|
|
69
|
+
// The number that answers "is anything writing this channel".
|
|
70
|
+
missing : inHubspot === 0 ? 0 : Math.max( 0, inHubspot - held )
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const flowing = Object.entries( report )
|
|
76
|
+
.filter( ( [ , value ] ) => value.inHubspot > 0 )
|
|
77
|
+
.map( ( [ key ] ) => key );
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
byChannel : report,
|
|
81
|
+
// Empty means nothing outside email, calls, meetings, notes and tasks is
|
|
82
|
+
// reaching HubSpot at all - which is a setup answer, not a pipeline answer.
|
|
83
|
+
flowing : flowing.length ? flowing : 'nothing beyond email/calls/meetings/notes/tasks',
|
|
84
|
+
note : 'LINKEDIN_MESSAGE at 0 means no tool is writing LinkedIn messages into'
|
|
85
|
+
+ ' HubSpot. Whatever writes them, this pipeline already reads them.'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
});
|