@drawbridge/drawbridge-utils 0.0.135 → 0.0.137
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 +149 -101
- package/dist/connections/index.d.cts +67 -13
- package/dist/connections/index.d.ts +67 -13
- package/dist/connections/index.js +149 -101
- package/dist/pricing.cjs +727 -695
- package/dist/pricing.d.cts +53 -5
- package/dist/pricing.d.ts +53 -5
- package/dist/pricing.js +726 -695
- package/dist/providers.cjs +149 -101
- package/dist/providers.d.cts +4 -4
- package/dist/providers.d.ts +4 -4
- package/dist/providers.js +149 -101
- package/package.json +2 -2
|
@@ -1125,6 +1125,74 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
|
|
|
1125
1125
|
</defs>
|
|
1126
1126
|
</svg>`;
|
|
1127
1127
|
|
|
1128
|
+
// lib/transactions.js
|
|
1129
|
+
var import_drawbridge_telemetry = require("@drawbridge/drawbridge-telemetry");
|
|
1130
|
+
|
|
1131
|
+
// lib/billing.js
|
|
1132
|
+
var import_drawbridge_telemetry2 = require("@drawbridge/drawbridge-telemetry");
|
|
1133
|
+
var logger = (0, import_drawbridge_telemetry2.createLogger)();
|
|
1134
|
+
var MARKUP = 1.3;
|
|
1135
|
+
var cost = {
|
|
1136
|
+
// gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
|
|
1137
|
+
// $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
|
|
1138
|
+
// output). ~3.6x the retired 2.5-flash output rate.
|
|
1139
|
+
"gemini-3.5-flash": {
|
|
1140
|
+
cached: 15,
|
|
1141
|
+
input: 150,
|
|
1142
|
+
output: 900
|
|
1143
|
+
},
|
|
1144
|
+
// gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
|
|
1145
|
+
// $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
|
|
1146
|
+
// output). A fifth of flash on input, ~a quarter on output. Growth's assistant
|
|
1147
|
+
// ranks the feed on this tier — one call per page — so its rows were the
|
|
1148
|
+
// unpriced ones until now.
|
|
1149
|
+
"gemini-3.5-flash-lite": {
|
|
1150
|
+
cached: 3,
|
|
1151
|
+
input: 30,
|
|
1152
|
+
output: 250
|
|
1153
|
+
},
|
|
1154
|
+
"gemini-2.5-flash": {
|
|
1155
|
+
cached: 3,
|
|
1156
|
+
input: 30,
|
|
1157
|
+
output: 250
|
|
1158
|
+
},
|
|
1159
|
+
"gemini-2.5-flash-image": {
|
|
1160
|
+
cached: 3,
|
|
1161
|
+
input: 30,
|
|
1162
|
+
output: 3e3
|
|
1163
|
+
},
|
|
1164
|
+
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
1165
|
+
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
1166
|
+
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
1167
|
+
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
1168
|
+
// that reproduces the per-image price from the tokens usageMetadata
|
|
1169
|
+
// reports. Growth's hero generation runs this model today.
|
|
1170
|
+
"gemini-3-pro-image-preview": {
|
|
1171
|
+
cached: 20,
|
|
1172
|
+
input: 200,
|
|
1173
|
+
output: 12e3
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
var toolCost = {
|
|
1177
|
+
search: 3.5
|
|
1178
|
+
};
|
|
1179
|
+
var toolPricing = Object.fromEntries(
|
|
1180
|
+
Object.entries(toolCost).map(([tool, value]) => [
|
|
1181
|
+
tool,
|
|
1182
|
+
Math.ceil(value * MARKUP)
|
|
1183
|
+
])
|
|
1184
|
+
);
|
|
1185
|
+
var pricing = Object.fromEntries(
|
|
1186
|
+
Object.entries(cost).map(([model, rates]) => [
|
|
1187
|
+
model,
|
|
1188
|
+
{
|
|
1189
|
+
cached: Math.round(rates.cached * MARKUP),
|
|
1190
|
+
input: Math.round(rates.input * MARKUP),
|
|
1191
|
+
output: Math.round(rates.output * MARKUP)
|
|
1192
|
+
}
|
|
1193
|
+
])
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1128
1196
|
// lib/features.js
|
|
1129
1197
|
var page = {
|
|
1130
1198
|
qrcode: {
|
|
@@ -1587,74 +1655,6 @@ var conversionRate = (subscription) => {
|
|
|
1587
1655
|
return ((_a = resolvePlan(subscription)) == null ? void 0 : _a.conversion) ?? free.conversion;
|
|
1588
1656
|
};
|
|
1589
1657
|
|
|
1590
|
-
// lib/transactions.js
|
|
1591
|
-
var import_drawbridge_telemetry = require("@drawbridge/drawbridge-telemetry");
|
|
1592
|
-
|
|
1593
|
-
// lib/billing.js
|
|
1594
|
-
var import_drawbridge_telemetry2 = require("@drawbridge/drawbridge-telemetry");
|
|
1595
|
-
var logger = (0, import_drawbridge_telemetry2.createLogger)();
|
|
1596
|
-
var MARKUP = 1.3;
|
|
1597
|
-
var cost = {
|
|
1598
|
-
// gemini-3.5-flash — verified against Google's pricing page 2026-07-09:
|
|
1599
|
-
// $0.15 cached / $1.50 input / $9.00 output per 1M tokens (thinking billed at
|
|
1600
|
-
// output). ~3.6x the retired 2.5-flash output rate.
|
|
1601
|
-
"gemini-3.5-flash": {
|
|
1602
|
-
cached: 15,
|
|
1603
|
-
input: 150,
|
|
1604
|
-
output: 900
|
|
1605
|
-
},
|
|
1606
|
-
// gemini-3.5-flash-lite — verified against Google's pricing page 2026-08-20:
|
|
1607
|
-
// $0.03 cached / $0.30 input / $2.50 output per 1M tokens (thinking billed at
|
|
1608
|
-
// output). A fifth of flash on input, ~a quarter on output. Growth's assistant
|
|
1609
|
-
// ranks the feed on this tier — one call per page — so its rows were the
|
|
1610
|
-
// unpriced ones until now.
|
|
1611
|
-
"gemini-3.5-flash-lite": {
|
|
1612
|
-
cached: 3,
|
|
1613
|
-
input: 30,
|
|
1614
|
-
output: 250
|
|
1615
|
-
},
|
|
1616
|
-
"gemini-2.5-flash": {
|
|
1617
|
-
cached: 3,
|
|
1618
|
-
input: 30,
|
|
1619
|
-
output: 250
|
|
1620
|
-
},
|
|
1621
|
-
"gemini-2.5-flash-image": {
|
|
1622
|
-
cached: 3,
|
|
1623
|
-
input: 30,
|
|
1624
|
-
output: 3e3
|
|
1625
|
-
},
|
|
1626
|
-
// gemini-3-pro-image-preview — verified against Google's pricing page
|
|
1627
|
-
// 2026-08-12: $2.00 input / $12.00 text output per 1M, and image output
|
|
1628
|
-
// tokens at ~$120/1M (a 1K-2K image is 1120 tokens = $0.134, a 4K image
|
|
1629
|
-
// 2000 tokens = $0.24). Encoded the flash-image way: one flat output rate
|
|
1630
|
-
// that reproduces the per-image price from the tokens usageMetadata
|
|
1631
|
-
// reports. Growth's hero generation runs this model today.
|
|
1632
|
-
"gemini-3-pro-image-preview": {
|
|
1633
|
-
cached: 20,
|
|
1634
|
-
input: 200,
|
|
1635
|
-
output: 12e3
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
var toolCost = {
|
|
1639
|
-
search: 3.5
|
|
1640
|
-
};
|
|
1641
|
-
var toolPricing = Object.fromEntries(
|
|
1642
|
-
Object.entries(toolCost).map(([tool, value]) => [
|
|
1643
|
-
tool,
|
|
1644
|
-
Math.ceil(value * MARKUP)
|
|
1645
|
-
])
|
|
1646
|
-
);
|
|
1647
|
-
var pricing = Object.fromEntries(
|
|
1648
|
-
Object.entries(cost).map(([model, rates]) => [
|
|
1649
|
-
model,
|
|
1650
|
-
{
|
|
1651
|
-
cached: Math.round(rates.cached * MARKUP),
|
|
1652
|
-
input: Math.round(rates.input * MARKUP),
|
|
1653
|
-
output: Math.round(rates.output * MARKUP)
|
|
1654
|
-
}
|
|
1655
|
-
])
|
|
1656
|
-
);
|
|
1657
|
-
|
|
1658
1658
|
// lib/pricing.js
|
|
1659
1659
|
var emailPlans = {
|
|
1660
1660
|
essentials50k: {
|
|
@@ -1724,6 +1724,33 @@ var sending = {
|
|
|
1724
1724
|
plan: emailPlan,
|
|
1725
1725
|
plans: emailPlans,
|
|
1726
1726
|
title: emailPlans[emailPlan].title
|
|
1727
|
+
},
|
|
1728
|
+
// What one outbound SMS SEGMENT costs us, against what channels.sms below
|
|
1729
|
+
// sells it for. Here for the same reason the email figures are: this file is
|
|
1730
|
+
// where a rate is found without knowing whose module owns it — and its
|
|
1731
|
+
// absence is why "does an SMS action cover its cost" could not be answered
|
|
1732
|
+
// from this repo at all.
|
|
1733
|
+
//
|
|
1734
|
+
// US toll-free outbound, read from Twilio's SMS pricing page 2026-09-03
|
|
1735
|
+
// (https://www.twilio.com/en-us/sms/pricing/us): $0.0083 per segment, plus a
|
|
1736
|
+
// carrier fee that varies by the DESTINATION carrier — $0.0035 AT&T, $0.0045
|
|
1737
|
+
// T-Mobile / Verizon / US Cellular, $0.004 everywhere else. CENTS per
|
|
1738
|
+
// segment, like every other rate in this file.
|
|
1739
|
+
//
|
|
1740
|
+
// WORST CASE, deliberately. A price floor computed from the average is a
|
|
1741
|
+
// floor that half of real traffic sits underneath.
|
|
1742
|
+
//
|
|
1743
|
+
// A segment is 153 GSM-7 characters once a message spans more than one — or
|
|
1744
|
+
// 67 if the body contains a single emoji or curly quote, which flips the
|
|
1745
|
+
// whole message to UCS-2. Twilio reports the count as `num_segments` on the
|
|
1746
|
+
// Message resource, which is what the send bills on.
|
|
1747
|
+
sms: {
|
|
1748
|
+
baseCents: 0.83,
|
|
1749
|
+
// The dearest carrier fee, not the mean: see WORST CASE above.
|
|
1750
|
+
carrierCents: 0.45,
|
|
1751
|
+
// Stated, not summed — 0.83 + 0.45 is 1.2800000000000002 in binary
|
|
1752
|
+
// floating point, and this number is compared against money.
|
|
1753
|
+
segmentCostCents: 1.28
|
|
1727
1754
|
}
|
|
1728
1755
|
};
|
|
1729
1756
|
var channels = {
|
|
@@ -1739,6 +1766,9 @@ var channels = {
|
|
|
1739
1766
|
includedInAllowance: false
|
|
1740
1767
|
}
|
|
1741
1768
|
};
|
|
1769
|
+
var MINIMUM_ACTION_CENTS = Math.round(
|
|
1770
|
+
sending.sms.segmentCostCents / channels.sms.actionsPerSegment * MARKUP * 1e3
|
|
1771
|
+
) / 1e3;
|
|
1742
1772
|
|
|
1743
1773
|
// lib/connections/providers/drawbridge.js
|
|
1744
1774
|
var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
|
|
@@ -1763,13 +1793,16 @@ var teamRecipients = async ({ memberIds = [], organization: organization2, read
|
|
|
1763
1793
|
]
|
|
1764
1794
|
}) : [];
|
|
1765
1795
|
const seen = /* @__PURE__ */ new Set();
|
|
1766
|
-
return
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1796
|
+
return {
|
|
1797
|
+
org,
|
|
1798
|
+
recipients: [owner, ...members].filter((member) => {
|
|
1799
|
+
if (!(member == null ? void 0 : member.id) || !(member == null ? void 0 : member.email)) return false;
|
|
1800
|
+
const address = member.email.toLowerCase();
|
|
1801
|
+
if (seen.has(address)) return false;
|
|
1802
|
+
seen.add(address);
|
|
1803
|
+
return true;
|
|
1804
|
+
})
|
|
1805
|
+
};
|
|
1773
1806
|
};
|
|
1774
1807
|
var queueNotification = (data2) => ({ collection: "notification", data: data2, operation: "create" });
|
|
1775
1808
|
var drawbridge_default2 = {
|
|
@@ -1845,7 +1878,7 @@ var drawbridge_default2 = {
|
|
|
1845
1878
|
const count = Number((counted == null ? void 0 : counted.count) || 0);
|
|
1846
1879
|
const request2 = { campaign, count, days };
|
|
1847
1880
|
if (!count) return { message: "No new leads in the period \u2014 digest skipped.", request: request2, response: { skipped: true }, skipped: true };
|
|
1848
|
-
const recipients = await teamRecipients({
|
|
1881
|
+
const { recipients } = await teamRecipients({
|
|
1849
1882
|
memberIds: ((_d = step.settings) == null ? void 0 : _d.members) || [],
|
|
1850
1883
|
organization: workflow.organization,
|
|
1851
1884
|
read
|
|
@@ -1881,7 +1914,7 @@ var drawbridge_default2 = {
|
|
|
1881
1914
|
var _a;
|
|
1882
1915
|
const memberIds = ((_a = step.settings) == null ? void 0 : _a.members) || [];
|
|
1883
1916
|
const request2 = { members: memberIds.length };
|
|
1884
|
-
const recipients = await teamRecipients({ memberIds, organization: workflow.organization, read });
|
|
1917
|
+
const { org, recipients } = await teamRecipients({ memberIds, organization: workflow.organization, read });
|
|
1885
1918
|
if (!recipients.length) {
|
|
1886
1919
|
return {
|
|
1887
1920
|
message: "No owner or accepted member with an email address \u2014 team notification skipped.",
|
|
@@ -1891,32 +1924,37 @@ var drawbridge_default2 = {
|
|
|
1891
1924
|
};
|
|
1892
1925
|
}
|
|
1893
1926
|
const bucket = Math.floor(Date.now() / (15 * 60 * 1e3));
|
|
1927
|
+
const campaign = (context == null ? void 0 : context.campaign) ? await read.get({ collection: "campaign", query: { id: context.campaign } }) : null;
|
|
1928
|
+
const link = (context == null ? void 0 : context.campaign) && (context == null ? void 0 : context.lead) ? {
|
|
1929
|
+
href: "/organizations/" + workflow.organization + "/campaigns/" + context.campaign + "/leads/" + context.lead,
|
|
1930
|
+
text: "View lead"
|
|
1931
|
+
} : null;
|
|
1932
|
+
const body = ((context == null ? void 0 : context.email) || "Someone") + " just entered" + ((campaign == null ? void 0 : campaign.title) ? " " + campaign.title : "") + ((org == null ? void 0 : org.title) ? " at " + org.title : "") + ".";
|
|
1933
|
+
const subject = "New lead" + ((campaign == null ? void 0 : campaign.title) ? " \u2014 " + campaign.title : "");
|
|
1894
1934
|
return {
|
|
1895
1935
|
message: "Team notification queued for " + recipients.length + " recipient(s).",
|
|
1896
1936
|
request: request2,
|
|
1897
1937
|
response: { notified: recipients.length },
|
|
1898
|
-
writes: recipients.map((member) => {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
};
|
|
1919
|
-
})
|
|
1938
|
+
writes: recipients.map((member) => ({
|
|
1939
|
+
...queueNotification({
|
|
1940
|
+
audience: "member",
|
|
1941
|
+
// Per workflow, recipient AND bucket, so one recipient's damper
|
|
1942
|
+
// can never swallow another's mail and a later bucket is never
|
|
1943
|
+
// mistaken for a duplicate of an earlier one.
|
|
1944
|
+
key: "team.notify." + workflow.id + "." + member.id + "." + bucket,
|
|
1945
|
+
link,
|
|
1946
|
+
message: body,
|
|
1947
|
+
organization: workflow.organization,
|
|
1948
|
+
send: { type: "email", email: member.email },
|
|
1949
|
+
title: subject,
|
|
1950
|
+
workflow: workflow.id
|
|
1951
|
+
}),
|
|
1952
|
+
// E11000 IS THE DAMPER WORKING: this recipient has already been
|
|
1953
|
+
// told within the bucket. Declared per write rather than assumed by
|
|
1954
|
+
// the shell, because on every other write here a duplicate key is a
|
|
1955
|
+
// real failure.
|
|
1956
|
+
ignoreDuplicate: true
|
|
1957
|
+
}))
|
|
1920
1958
|
};
|
|
1921
1959
|
},
|
|
1922
1960
|
// Drawbridge sends lead-facing email itself — no merchant provider gates
|
|
@@ -2322,14 +2360,24 @@ var drawbridge_default2 = {
|
|
|
2322
2360
|
}),
|
|
2323
2361
|
// To organization MEMBERS. Never suppressed — an entrant's opt-out must
|
|
2324
2362
|
// not silence an alert to staff — and not billed.
|
|
2363
|
+
//
|
|
2364
|
+
// ITS COPY IS WRITTEN, NOT ASKED FOR. This says the same thing every
|
|
2365
|
+
// time — a lead arrived, here it is — so the merchant chooses who hears
|
|
2366
|
+
// it, not how it reads, and the hook composes the subject, the body and
|
|
2367
|
+
// the link to the lead.
|
|
2368
|
+
//
|
|
2369
|
+
// `message` and `subject` stay declared, optional and IGNORED: stored
|
|
2370
|
+
// workflows still carry them and the api validates settings with
|
|
2371
|
+
// noUnknown().strict(), so dropping the keys would 400 the next save of
|
|
2372
|
+
// a workflow built before this.
|
|
2325
2373
|
notify: () => ({
|
|
2326
2374
|
hook: "email.notify",
|
|
2327
2375
|
key: "Email \u2014 Notification",
|
|
2328
2376
|
queue: "notification",
|
|
2329
2377
|
settings: {
|
|
2330
2378
|
members: { of: "string", type: "array" },
|
|
2331
|
-
message: {
|
|
2332
|
-
subject: {
|
|
2379
|
+
message: { type: "string" },
|
|
2380
|
+
subject: { type: "string" }
|
|
2333
2381
|
},
|
|
2334
2382
|
triggers: ["lead.insert"],
|
|
2335
2383
|
// Zero is a PRICE, and a deliberate one. Declared rather than omitted
|
|
@@ -1663,19 +1663,25 @@ const teamRecipients = async ({ memberIds = [], organization, read }) => {
|
|
|
1663
1663
|
// different teammates collapse into one and the second is never told.
|
|
1664
1664
|
const seen = new Set();
|
|
1665
1665
|
|
|
1666
|
-
|
|
1666
|
+
// THE ORG DOCUMENT COMES BACK TOO. It was read here anyway, and notify names
|
|
1667
|
+
// the organization in its copy — a teammate can belong to more than one, so
|
|
1668
|
+
// re-reading it there would be a second query for a document this already has.
|
|
1669
|
+
return {
|
|
1670
|
+
org,
|
|
1671
|
+
recipients : [ owner, ...members ].filter( ( member ) => {
|
|
1667
1672
|
|
|
1668
|
-
|
|
1673
|
+
if( ! member?.id || ! member?.email ) return false;
|
|
1669
1674
|
|
|
1670
|
-
|
|
1675
|
+
const address = member.email.toLowerCase();
|
|
1671
1676
|
|
|
1672
|
-
|
|
1677
|
+
if( seen.has( address ) ) return false;
|
|
1673
1678
|
|
|
1674
|
-
|
|
1679
|
+
seen.add( address );
|
|
1675
1680
|
|
|
1676
|
-
|
|
1681
|
+
return true;
|
|
1677
1682
|
|
|
1678
|
-
|
|
1683
|
+
})
|
|
1684
|
+
};
|
|
1679
1685
|
|
|
1680
1686
|
};
|
|
1681
1687
|
|
|
@@ -1768,7 +1774,7 @@ var drawbridge = {
|
|
|
1768
1774
|
// mail the merchant did not ask for and would learn to ignore.
|
|
1769
1775
|
if( ! count ) return { message : 'No new leads in the period — digest skipped.', request, response : { skipped : true }, skipped : true };
|
|
1770
1776
|
|
|
1771
|
-
const recipients = await teamRecipients({
|
|
1777
|
+
const { recipients } = await teamRecipients({
|
|
1772
1778
|
memberIds : step.settings?.members || [],
|
|
1773
1779
|
organization : workflow.organization,
|
|
1774
1780
|
read
|
|
@@ -1809,7 +1815,7 @@ var drawbridge = {
|
|
|
1809
1815
|
|
|
1810
1816
|
const request = { members : memberIds.length };
|
|
1811
1817
|
|
|
1812
|
-
const recipients = await teamRecipients({ memberIds, organization : workflow.organization, read });
|
|
1818
|
+
const { org, recipients } = await teamRecipients({ memberIds, organization : workflow.organization, read });
|
|
1813
1819
|
|
|
1814
1820
|
if( ! recipients.length ){
|
|
1815
1821
|
|
|
@@ -1842,6 +1848,43 @@ var drawbridge = {
|
|
|
1842
1848
|
// to redefine from here.
|
|
1843
1849
|
const bucket = Math.floor( Date.now() / ( 15 * 60 * 1000 ) );
|
|
1844
1850
|
|
|
1851
|
+
// THE COPY IS OURS, not the merchant's — `step.settings.message` and
|
|
1852
|
+
// `.subject` are ignored here on purpose. The whole message is "a lead
|
|
1853
|
+
// arrived, go and look", so the useful part is the link, not the
|
|
1854
|
+
// wording.
|
|
1855
|
+
//
|
|
1856
|
+
// It NAMES THE CAMPAIGN AND THE ORGANIZATION, because a recipient can
|
|
1857
|
+
// be watching several of both and a subject line reading only "New
|
|
1858
|
+
// lead" makes them open the mail to find out which. The campaign title
|
|
1859
|
+
// is the one extra read this step makes; the org came back with the
|
|
1860
|
+
// recipients.
|
|
1861
|
+
const campaign = context?.campaign
|
|
1862
|
+
? await read.get({ collection : 'campaign', query : { id : context.campaign } })
|
|
1863
|
+
: null;
|
|
1864
|
+
|
|
1865
|
+
// The href is DASHBOARD-RELATIVE: a manifest must not read
|
|
1866
|
+
// process.env (see lib/connections/index.js), so the deployment's base
|
|
1867
|
+
// url is applied by the sender — drawbridge-sync's queue/notification.js
|
|
1868
|
+
// — before it renders. Absent a campaign or lead on the context there
|
|
1869
|
+
// is nothing to point at, and no button is drawn.
|
|
1870
|
+
const link = ( context?.campaign && context?.lead )
|
|
1871
|
+
? {
|
|
1872
|
+
href : '/organizations/' + workflow.organization + '/campaigns/' + context.campaign + '/leads/' + context.lead,
|
|
1873
|
+
text : 'View lead'
|
|
1874
|
+
}
|
|
1875
|
+
: null;
|
|
1876
|
+
|
|
1877
|
+
// Every part is optional and every part is dropped when it is missing,
|
|
1878
|
+
// so the worst case reads 'Someone just entered.' rather than a
|
|
1879
|
+
// sentence with holes in it.
|
|
1880
|
+
const body = ( context?.email || 'Someone' )
|
|
1881
|
+
+ ' just entered'
|
|
1882
|
+
+ ( campaign?.title ? ' ' + campaign.title : '' )
|
|
1883
|
+
+ ( org?.title ? ' at ' + org.title : '' )
|
|
1884
|
+
+ '.';
|
|
1885
|
+
|
|
1886
|
+
const subject = 'New lead' + ( campaign?.title ? ' — ' + campaign.title : '' );
|
|
1887
|
+
|
|
1845
1888
|
return {
|
|
1846
1889
|
message : 'Team notification queued for ' + recipients.length + ' recipient(s).',
|
|
1847
1890
|
request,
|
|
@@ -1853,10 +1896,11 @@ var drawbridge = {
|
|
|
1853
1896
|
// can never swallow another's mail and a later bucket is never
|
|
1854
1897
|
// mistaken for a duplicate of an earlier one.
|
|
1855
1898
|
key : 'team.notify.' + workflow.id + '.' + member.id + '.' + bucket,
|
|
1856
|
-
|
|
1899
|
+
link,
|
|
1900
|
+
message : body,
|
|
1857
1901
|
organization : workflow.organization,
|
|
1858
1902
|
send : { type : 'email', email : member.email },
|
|
1859
|
-
title :
|
|
1903
|
+
title : subject,
|
|
1860
1904
|
workflow : workflow.id
|
|
1861
1905
|
}),
|
|
1862
1906
|
// E11000 IS THE DAMPER WORKING: this recipient has already been
|
|
@@ -2416,14 +2460,24 @@ var drawbridge = {
|
|
|
2416
2460
|
|
|
2417
2461
|
// To organization MEMBERS. Never suppressed — an entrant's opt-out must
|
|
2418
2462
|
// not silence an alert to staff — and not billed.
|
|
2463
|
+
//
|
|
2464
|
+
// ITS COPY IS WRITTEN, NOT ASKED FOR. This says the same thing every
|
|
2465
|
+
// time — a lead arrived, here it is — so the merchant chooses who hears
|
|
2466
|
+
// it, not how it reads, and the hook composes the subject, the body and
|
|
2467
|
+
// the link to the lead.
|
|
2468
|
+
//
|
|
2469
|
+
// `message` and `subject` stay declared, optional and IGNORED: stored
|
|
2470
|
+
// workflows still carry them and the api validates settings with
|
|
2471
|
+
// noUnknown().strict(), so dropping the keys would 400 the next save of
|
|
2472
|
+
// a workflow built before this.
|
|
2419
2473
|
notify : () => ({
|
|
2420
2474
|
hook : 'email.notify',
|
|
2421
2475
|
key : 'Email — Notification',
|
|
2422
2476
|
queue : 'notification',
|
|
2423
2477
|
settings : {
|
|
2424
2478
|
members : { of : 'string', type : 'array' },
|
|
2425
|
-
message : {
|
|
2426
|
-
subject : {
|
|
2479
|
+
message : { type : 'string' },
|
|
2480
|
+
subject : { type : 'string' }
|
|
2427
2481
|
},
|
|
2428
2482
|
triggers : [ 'lead.insert' ],
|
|
2429
2483
|
// Zero is a PRICE, and a deliberate one. Declared rather than omitted
|
|
@@ -1663,19 +1663,25 @@ const teamRecipients = async ({ memberIds = [], organization, read }) => {
|
|
|
1663
1663
|
// different teammates collapse into one and the second is never told.
|
|
1664
1664
|
const seen = new Set();
|
|
1665
1665
|
|
|
1666
|
-
|
|
1666
|
+
// THE ORG DOCUMENT COMES BACK TOO. It was read here anyway, and notify names
|
|
1667
|
+
// the organization in its copy — a teammate can belong to more than one, so
|
|
1668
|
+
// re-reading it there would be a second query for a document this already has.
|
|
1669
|
+
return {
|
|
1670
|
+
org,
|
|
1671
|
+
recipients : [ owner, ...members ].filter( ( member ) => {
|
|
1667
1672
|
|
|
1668
|
-
|
|
1673
|
+
if( ! member?.id || ! member?.email ) return false;
|
|
1669
1674
|
|
|
1670
|
-
|
|
1675
|
+
const address = member.email.toLowerCase();
|
|
1671
1676
|
|
|
1672
|
-
|
|
1677
|
+
if( seen.has( address ) ) return false;
|
|
1673
1678
|
|
|
1674
|
-
|
|
1679
|
+
seen.add( address );
|
|
1675
1680
|
|
|
1676
|
-
|
|
1681
|
+
return true;
|
|
1677
1682
|
|
|
1678
|
-
|
|
1683
|
+
})
|
|
1684
|
+
};
|
|
1679
1685
|
|
|
1680
1686
|
};
|
|
1681
1687
|
|
|
@@ -1768,7 +1774,7 @@ var drawbridge = {
|
|
|
1768
1774
|
// mail the merchant did not ask for and would learn to ignore.
|
|
1769
1775
|
if( ! count ) return { message : 'No new leads in the period — digest skipped.', request, response : { skipped : true }, skipped : true };
|
|
1770
1776
|
|
|
1771
|
-
const recipients = await teamRecipients({
|
|
1777
|
+
const { recipients } = await teamRecipients({
|
|
1772
1778
|
memberIds : step.settings?.members || [],
|
|
1773
1779
|
organization : workflow.organization,
|
|
1774
1780
|
read
|
|
@@ -1809,7 +1815,7 @@ var drawbridge = {
|
|
|
1809
1815
|
|
|
1810
1816
|
const request = { members : memberIds.length };
|
|
1811
1817
|
|
|
1812
|
-
const recipients = await teamRecipients({ memberIds, organization : workflow.organization, read });
|
|
1818
|
+
const { org, recipients } = await teamRecipients({ memberIds, organization : workflow.organization, read });
|
|
1813
1819
|
|
|
1814
1820
|
if( ! recipients.length ){
|
|
1815
1821
|
|
|
@@ -1842,6 +1848,43 @@ var drawbridge = {
|
|
|
1842
1848
|
// to redefine from here.
|
|
1843
1849
|
const bucket = Math.floor( Date.now() / ( 15 * 60 * 1000 ) );
|
|
1844
1850
|
|
|
1851
|
+
// THE COPY IS OURS, not the merchant's — `step.settings.message` and
|
|
1852
|
+
// `.subject` are ignored here on purpose. The whole message is "a lead
|
|
1853
|
+
// arrived, go and look", so the useful part is the link, not the
|
|
1854
|
+
// wording.
|
|
1855
|
+
//
|
|
1856
|
+
// It NAMES THE CAMPAIGN AND THE ORGANIZATION, because a recipient can
|
|
1857
|
+
// be watching several of both and a subject line reading only "New
|
|
1858
|
+
// lead" makes them open the mail to find out which. The campaign title
|
|
1859
|
+
// is the one extra read this step makes; the org came back with the
|
|
1860
|
+
// recipients.
|
|
1861
|
+
const campaign = context?.campaign
|
|
1862
|
+
? await read.get({ collection : 'campaign', query : { id : context.campaign } })
|
|
1863
|
+
: null;
|
|
1864
|
+
|
|
1865
|
+
// The href is DASHBOARD-RELATIVE: a manifest must not read
|
|
1866
|
+
// process.env (see lib/connections/index.js), so the deployment's base
|
|
1867
|
+
// url is applied by the sender — drawbridge-sync's queue/notification.js
|
|
1868
|
+
// — before it renders. Absent a campaign or lead on the context there
|
|
1869
|
+
// is nothing to point at, and no button is drawn.
|
|
1870
|
+
const link = ( context?.campaign && context?.lead )
|
|
1871
|
+
? {
|
|
1872
|
+
href : '/organizations/' + workflow.organization + '/campaigns/' + context.campaign + '/leads/' + context.lead,
|
|
1873
|
+
text : 'View lead'
|
|
1874
|
+
}
|
|
1875
|
+
: null;
|
|
1876
|
+
|
|
1877
|
+
// Every part is optional and every part is dropped when it is missing,
|
|
1878
|
+
// so the worst case reads 'Someone just entered.' rather than a
|
|
1879
|
+
// sentence with holes in it.
|
|
1880
|
+
const body = ( context?.email || 'Someone' )
|
|
1881
|
+
+ ' just entered'
|
|
1882
|
+
+ ( campaign?.title ? ' ' + campaign.title : '' )
|
|
1883
|
+
+ ( org?.title ? ' at ' + org.title : '' )
|
|
1884
|
+
+ '.';
|
|
1885
|
+
|
|
1886
|
+
const subject = 'New lead' + ( campaign?.title ? ' — ' + campaign.title : '' );
|
|
1887
|
+
|
|
1845
1888
|
return {
|
|
1846
1889
|
message : 'Team notification queued for ' + recipients.length + ' recipient(s).',
|
|
1847
1890
|
request,
|
|
@@ -1853,10 +1896,11 @@ var drawbridge = {
|
|
|
1853
1896
|
// can never swallow another's mail and a later bucket is never
|
|
1854
1897
|
// mistaken for a duplicate of an earlier one.
|
|
1855
1898
|
key : 'team.notify.' + workflow.id + '.' + member.id + '.' + bucket,
|
|
1856
|
-
|
|
1899
|
+
link,
|
|
1900
|
+
message : body,
|
|
1857
1901
|
organization : workflow.organization,
|
|
1858
1902
|
send : { type : 'email', email : member.email },
|
|
1859
|
-
title :
|
|
1903
|
+
title : subject,
|
|
1860
1904
|
workflow : workflow.id
|
|
1861
1905
|
}),
|
|
1862
1906
|
// E11000 IS THE DAMPER WORKING: this recipient has already been
|
|
@@ -2416,14 +2460,24 @@ var drawbridge = {
|
|
|
2416
2460
|
|
|
2417
2461
|
// To organization MEMBERS. Never suppressed — an entrant's opt-out must
|
|
2418
2462
|
// not silence an alert to staff — and not billed.
|
|
2463
|
+
//
|
|
2464
|
+
// ITS COPY IS WRITTEN, NOT ASKED FOR. This says the same thing every
|
|
2465
|
+
// time — a lead arrived, here it is — so the merchant chooses who hears
|
|
2466
|
+
// it, not how it reads, and the hook composes the subject, the body and
|
|
2467
|
+
// the link to the lead.
|
|
2468
|
+
//
|
|
2469
|
+
// `message` and `subject` stay declared, optional and IGNORED: stored
|
|
2470
|
+
// workflows still carry them and the api validates settings with
|
|
2471
|
+
// noUnknown().strict(), so dropping the keys would 400 the next save of
|
|
2472
|
+
// a workflow built before this.
|
|
2419
2473
|
notify : () => ({
|
|
2420
2474
|
hook : 'email.notify',
|
|
2421
2475
|
key : 'Email — Notification',
|
|
2422
2476
|
queue : 'notification',
|
|
2423
2477
|
settings : {
|
|
2424
2478
|
members : { of : 'string', type : 'array' },
|
|
2425
|
-
message : {
|
|
2426
|
-
subject : {
|
|
2479
|
+
message : { type : 'string' },
|
|
2480
|
+
subject : { type : 'string' }
|
|
2427
2481
|
},
|
|
2428
2482
|
triggers : [ 'lead.insert' ],
|
|
2429
2483
|
// Zero is a PRICE, and a deliberate one. Declared rather than omitted
|