mcp-scraper 0.32.2 → 0.32.3
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/README.md +1 -1
- package/dist/bin/api-server.cjs +420 -246
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +42 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/{chunk-XEED325T.js → chunk-GNRHBIYZ.js} +43 -2
- package/dist/chunk-GNRHBIYZ.js.map +1 -0
- package/dist/chunk-XF46AB5C.js +7 -0
- package/dist/chunk-XF46AB5C.js.map +1 -0
- package/dist/{server-LTZDJHFG.js → server-NHLEIKDZ.js} +372 -238
- package/dist/server-NHLEIKDZ.js.map +1 -0
- package/docs/mcp-tool-manifest.generated.json +165 -3
- package/package.json +1 -1
- package/dist/chunk-XEED325T.js.map +0 -1
- package/dist/chunk-XLO5CZ6T.js +0 -7
- package/dist/chunk-XLO5CZ6T.js.map +0 -1
- package/dist/server-LTZDJHFG.js.map +0 -1
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
sanitizeAttempts,
|
|
38
38
|
sanitizeHarvestResult,
|
|
39
39
|
transcribeMediaUrl
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-GNRHBIYZ.js";
|
|
41
41
|
import {
|
|
42
42
|
auditImages,
|
|
43
43
|
buildLinkReport,
|
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
RawMapsOverviewSchema,
|
|
79
79
|
RawMapsReviewStatsSchema
|
|
80
80
|
} from "./chunk-V73MPRU6.js";
|
|
81
|
-
import "./chunk-
|
|
81
|
+
import "./chunk-XF46AB5C.js";
|
|
82
82
|
import {
|
|
83
83
|
completeExtractJob,
|
|
84
84
|
countSuccessfulPages,
|
|
@@ -13097,6 +13097,22 @@ function buildGoogleOrganicSearchUrl(input) {
|
|
|
13097
13097
|
if (input.location) params.set("uule", encodeUule(normalizeLocation(input.location)));
|
|
13098
13098
|
return `https://www.google.com/search?${params.toString()}`;
|
|
13099
13099
|
}
|
|
13100
|
+
function mapsIdentifiersFromUrl(url) {
|
|
13101
|
+
const fid = url.match(/!1s(0x[0-9a-f]+):(0x[0-9a-f]+)/i) ?? url.match(/(0x[0-9a-f]+):(0x[0-9a-f]+)/i);
|
|
13102
|
+
if (fid) {
|
|
13103
|
+
try {
|
|
13104
|
+
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: BigInt(fid[2]).toString() };
|
|
13105
|
+
} catch {
|
|
13106
|
+
return { cid: `${fid[1]}:${fid[2]}`, cidDecimal: null };
|
|
13107
|
+
}
|
|
13108
|
+
}
|
|
13109
|
+
try {
|
|
13110
|
+
const decimal = new URL(url).searchParams.get("cid");
|
|
13111
|
+
return { cid: null, cidDecimal: decimal && /^\d+$/.test(decimal) ? decimal : null };
|
|
13112
|
+
} catch {
|
|
13113
|
+
return { cid: null, cidDecimal: null };
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13100
13116
|
var MapsSearchExtractor = class {
|
|
13101
13117
|
constructor(driver) {
|
|
13102
13118
|
this.driver = driver;
|
|
@@ -13172,13 +13188,15 @@ var MapsSearchExtractor = class {
|
|
|
13172
13188
|
for (const card of cards) {
|
|
13173
13189
|
if (results.length >= options.maxResults || seen.has(card.cardKey)) continue;
|
|
13174
13190
|
seen.add(card.cardKey);
|
|
13191
|
+
const cardIds = mapsIdentifiersFromUrl(card.directionsUrl ?? "");
|
|
13192
|
+
const cardPlaceUrl = cardIds.cidDecimal ? `https://www.google.com/maps?cid=${cardIds.cidDecimal}` : card.placeUrl;
|
|
13175
13193
|
const details = await this.openCardAndExtractDialog(page, card, options.includeServices);
|
|
13176
13194
|
results.push({
|
|
13177
13195
|
position: results.length + 1,
|
|
13178
13196
|
name: card.name,
|
|
13179
|
-
placeUrl: details?.placeUrl ??
|
|
13180
|
-
cid: details?.cid ??
|
|
13181
|
-
cidDecimal: details?.cidDecimal ??
|
|
13197
|
+
placeUrl: details?.placeUrl ?? cardPlaceUrl,
|
|
13198
|
+
cid: details?.cid ?? cardIds.cid,
|
|
13199
|
+
cidDecimal: details?.cidDecimal ?? cardIds.cidDecimal,
|
|
13182
13200
|
rating: details?.rating ?? card.rating,
|
|
13183
13201
|
reviewCount: details?.reviewCount ?? card.reviewCount,
|
|
13184
13202
|
category: details?.category ?? card.category,
|
|
@@ -13235,8 +13253,10 @@ var MapsSearchExtractor = class {
|
|
|
13235
13253
|
const phone = lines.map((line) => line.match(phonePattern)?.[0]).find((value) => Boolean(value)) ?? null;
|
|
13236
13254
|
const hoursStatus = normalize((lines.find((line) => /\b(?:open|closed|opens|closes)\b/i.test(line)) ?? "").split("\xB7")[0] ?? "");
|
|
13237
13255
|
const address = lines.find((line) => addressPattern.test(line)) ?? null;
|
|
13238
|
-
const
|
|
13256
|
+
const anchorHrefs = Array.from(card.querySelectorAll("a[href]")).map((anchor) => anchor.href);
|
|
13257
|
+
const websiteUrl = anchorHrefs.find((href) => /^https?:/i.test(href) && !/google\.|gstatic|googleusercontent/.test(href)) ?? null;
|
|
13239
13258
|
if (!rating && !(phone && websiteUrl)) continue;
|
|
13259
|
+
const fidHref = anchorHrefs.find((href) => /(0x[0-9a-f]+):(0x[0-9a-f]+)/i.test(href)) ?? null;
|
|
13240
13260
|
const key = `${name.toLowerCase()}|${lines.join(" ").toLowerCase()}`;
|
|
13241
13261
|
if (seen.has(key)) continue;
|
|
13242
13262
|
seen.add(key);
|
|
@@ -13256,7 +13276,7 @@ var MapsSearchExtractor = class {
|
|
|
13256
13276
|
phone,
|
|
13257
13277
|
hoursStatus,
|
|
13258
13278
|
websiteUrl,
|
|
13259
|
-
directionsUrl: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
|
|
13279
|
+
directionsUrl: fidHref ?? `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([name, address].filter(Boolean).join(", "))}`,
|
|
13260
13280
|
metadata: lines.slice(0, 20)
|
|
13261
13281
|
});
|
|
13262
13282
|
}
|
|
@@ -19554,9 +19574,222 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
|
|
|
19554
19574
|
}
|
|
19555
19575
|
|
|
19556
19576
|
// src/api/stripe-routes.ts
|
|
19557
|
-
import
|
|
19577
|
+
import Stripe2 from "stripe";
|
|
19558
19578
|
import { Hono as Hono19 } from "hono";
|
|
19559
|
-
|
|
19579
|
+
|
|
19580
|
+
// src/api/connected-account-billing.ts
|
|
19581
|
+
import Stripe from "stripe";
|
|
19582
|
+
var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
|
|
19583
|
+
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
19584
|
+
async function ensureConnectedAccountBillingSchema() {
|
|
19585
|
+
await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
|
|
19586
|
+
user_id INTEGER PRIMARY KEY,
|
|
19587
|
+
stripe_subscription_id TEXT,
|
|
19588
|
+
stripe_subscription_item_id TEXT,
|
|
19589
|
+
price_id TEXT,
|
|
19590
|
+
quantity INTEGER NOT NULL DEFAULT 0,
|
|
19591
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
19592
|
+
last_error_code TEXT,
|
|
19593
|
+
synced_at TEXT,
|
|
19594
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
19595
|
+
)`);
|
|
19596
|
+
}
|
|
19597
|
+
async function getConnectedAccountBillingState(userId) {
|
|
19598
|
+
await ensureConnectedAccountBillingSchema();
|
|
19599
|
+
const result = await getDb().execute({
|
|
19600
|
+
sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
|
|
19601
|
+
args: [Number(userId)]
|
|
19602
|
+
});
|
|
19603
|
+
return result.rows[0] ? result.rows[0] : null;
|
|
19604
|
+
}
|
|
19605
|
+
async function setConnectedAccountBillingState(input) {
|
|
19606
|
+
await ensureConnectedAccountBillingSchema();
|
|
19607
|
+
await getDb().execute({
|
|
19608
|
+
sql: `INSERT INTO connected_account_billing
|
|
19609
|
+
(user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
|
|
19610
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
|
|
19611
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
19612
|
+
stripe_subscription_id = excluded.stripe_subscription_id,
|
|
19613
|
+
stripe_subscription_item_id = excluded.stripe_subscription_item_id,
|
|
19614
|
+
price_id = excluded.price_id,
|
|
19615
|
+
quantity = excluded.quantity,
|
|
19616
|
+
status = excluded.status,
|
|
19617
|
+
last_error_code = excluded.last_error_code,
|
|
19618
|
+
synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
|
|
19619
|
+
updated_at = datetime('now')`,
|
|
19620
|
+
args: [
|
|
19621
|
+
Number(input.userId),
|
|
19622
|
+
input.stripeSubscriptionId,
|
|
19623
|
+
input.stripeSubscriptionItemId,
|
|
19624
|
+
input.priceId,
|
|
19625
|
+
input.quantity,
|
|
19626
|
+
input.status,
|
|
19627
|
+
input.lastErrorCode,
|
|
19628
|
+
input.synced ? 1 : 0,
|
|
19629
|
+
input.synced ? 1 : 0
|
|
19630
|
+
]
|
|
19631
|
+
});
|
|
19632
|
+
const state = await getConnectedAccountBillingState(input.userId);
|
|
19633
|
+
if (!state) throw new Error("connected account billing state was not persisted");
|
|
19634
|
+
return state;
|
|
19635
|
+
}
|
|
19636
|
+
var ConnectedAccountBillingError = class extends Error {
|
|
19637
|
+
constructor(message, code, status = 409) {
|
|
19638
|
+
super(message);
|
|
19639
|
+
this.code = code;
|
|
19640
|
+
this.status = status;
|
|
19641
|
+
this.name = "ConnectedAccountBillingError";
|
|
19642
|
+
}
|
|
19643
|
+
code;
|
|
19644
|
+
status;
|
|
19645
|
+
};
|
|
19646
|
+
function connectedAccountPriceId() {
|
|
19647
|
+
const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
|
|
19648
|
+
if (configured) return configured;
|
|
19649
|
+
return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
|
|
19650
|
+
}
|
|
19651
|
+
function connectedAccountBillingView(actualQuantity, state) {
|
|
19652
|
+
const quantity = Math.max(0, Math.round(actualQuantity));
|
|
19653
|
+
return {
|
|
19654
|
+
provider: "nango",
|
|
19655
|
+
billingMode: "flat_recurring_usd_plus_credits",
|
|
19656
|
+
priceId: connectedAccountPriceId(),
|
|
19657
|
+
unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
19658
|
+
interval: "month",
|
|
19659
|
+
quantity,
|
|
19660
|
+
projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
19661
|
+
status: state?.status ?? "pending",
|
|
19662
|
+
lastErrorCode: state?.last_error_code ?? null,
|
|
19663
|
+
syncedAt: state?.synced_at ?? null,
|
|
19664
|
+
usage: CONNECTED_USAGE_RATE_POLICY.usage
|
|
19665
|
+
};
|
|
19666
|
+
}
|
|
19667
|
+
function findBasePlanItem(subscription) {
|
|
19668
|
+
return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
|
|
19669
|
+
}
|
|
19670
|
+
function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
|
|
19671
|
+
return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
|
|
19672
|
+
}
|
|
19673
|
+
function liveSubscription(subscription) {
|
|
19674
|
+
return subscription.status === "active" || subscription.status === "trialing";
|
|
19675
|
+
}
|
|
19676
|
+
function safeStripeErrorCode(error) {
|
|
19677
|
+
if (error instanceof ConnectedAccountBillingError) return error.code;
|
|
19678
|
+
return "stripe_connection_billing_failed";
|
|
19679
|
+
}
|
|
19680
|
+
async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
|
|
19681
|
+
await setConnectedAccountBillingState({
|
|
19682
|
+
userId: user.id,
|
|
19683
|
+
stripeSubscriptionId: user.subscription_id,
|
|
19684
|
+
stripeSubscriptionItemId: subscriptionItemId,
|
|
19685
|
+
priceId: connectedAccountPriceId(),
|
|
19686
|
+
quantity: actualQuantity,
|
|
19687
|
+
status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
|
|
19688
|
+
lastErrorCode: safeStripeErrorCode(error),
|
|
19689
|
+
synced: false
|
|
19690
|
+
});
|
|
19691
|
+
}
|
|
19692
|
+
async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
|
|
19693
|
+
if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
|
|
19694
|
+
throw new Error("actualQuantity must be a non-negative safe integer");
|
|
19695
|
+
}
|
|
19696
|
+
const priceId = connectedAccountPriceId();
|
|
19697
|
+
if (!priceId) {
|
|
19698
|
+
const error = new ConnectedAccountBillingError(
|
|
19699
|
+
"Connected-account billing is not configured.",
|
|
19700
|
+
"connected_account_price_not_configured",
|
|
19701
|
+
503
|
|
19702
|
+
);
|
|
19703
|
+
await persistFailure(user, actualQuantity, error);
|
|
19704
|
+
throw error;
|
|
19705
|
+
}
|
|
19706
|
+
if (!user.subscription_id) {
|
|
19707
|
+
if (actualQuantity === 0) {
|
|
19708
|
+
const state = await setConnectedAccountBillingState({
|
|
19709
|
+
userId: user.id,
|
|
19710
|
+
stripeSubscriptionId: null,
|
|
19711
|
+
stripeSubscriptionItemId: null,
|
|
19712
|
+
priceId,
|
|
19713
|
+
quantity: 0,
|
|
19714
|
+
status: "synced",
|
|
19715
|
+
lastErrorCode: null,
|
|
19716
|
+
synced: true
|
|
19717
|
+
});
|
|
19718
|
+
return connectedAccountBillingView(0, state);
|
|
19719
|
+
}
|
|
19720
|
+
const error = new ConnectedAccountBillingError(
|
|
19721
|
+
"An active paid plan is required before connected accounts can be billed.",
|
|
19722
|
+
"paid_plan_required",
|
|
19723
|
+
403
|
|
19724
|
+
);
|
|
19725
|
+
await persistFailure(user, actualQuantity, error);
|
|
19726
|
+
throw error;
|
|
19727
|
+
}
|
|
19728
|
+
const client2 = stripeClient ?? (() => {
|
|
19729
|
+
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
19730
|
+
if (!secret2) {
|
|
19731
|
+
throw new ConnectedAccountBillingError(
|
|
19732
|
+
"Stripe is not configured.",
|
|
19733
|
+
"connected_account_price_not_configured",
|
|
19734
|
+
503
|
|
19735
|
+
);
|
|
19736
|
+
}
|
|
19737
|
+
return new Stripe(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
19738
|
+
})();
|
|
19739
|
+
let currentItem;
|
|
19740
|
+
try {
|
|
19741
|
+
const subscription = await client2.subscriptions.retrieve(user.subscription_id);
|
|
19742
|
+
currentItem = findConnectedAccountItem(subscription, priceId);
|
|
19743
|
+
if (!liveSubscription(subscription)) {
|
|
19744
|
+
throw new ConnectedAccountBillingError(
|
|
19745
|
+
"The plan subscription is not active.",
|
|
19746
|
+
"subscription_not_active",
|
|
19747
|
+
402
|
|
19748
|
+
);
|
|
19749
|
+
}
|
|
19750
|
+
if (!findBasePlanItem(subscription)) {
|
|
19751
|
+
throw new ConnectedAccountBillingError(
|
|
19752
|
+
"The base plan item could not be found on this subscription.",
|
|
19753
|
+
"subscription_plan_item_missing",
|
|
19754
|
+
409
|
|
19755
|
+
);
|
|
19756
|
+
}
|
|
19757
|
+
const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
|
|
19758
|
+
let resultingItem = currentItem;
|
|
19759
|
+
if (actualQuantity !== currentQuantity) {
|
|
19760
|
+
const updated = await client2.subscriptions.update(subscription.id, {
|
|
19761
|
+
items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
|
|
19762
|
+
proration_behavior: "create_prorations"
|
|
19763
|
+
});
|
|
19764
|
+
resultingItem = findConnectedAccountItem(updated, priceId);
|
|
19765
|
+
}
|
|
19766
|
+
const state = await setConnectedAccountBillingState({
|
|
19767
|
+
userId: user.id,
|
|
19768
|
+
stripeSubscriptionId: subscription.id,
|
|
19769
|
+
stripeSubscriptionItemId: resultingItem?.id ?? null,
|
|
19770
|
+
priceId,
|
|
19771
|
+
quantity: actualQuantity,
|
|
19772
|
+
status: "synced",
|
|
19773
|
+
lastErrorCode: null,
|
|
19774
|
+
synced: true
|
|
19775
|
+
});
|
|
19776
|
+
return connectedAccountBillingView(actualQuantity, state);
|
|
19777
|
+
} catch (error) {
|
|
19778
|
+
await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
|
|
19779
|
+
if (error instanceof ConnectedAccountBillingError) throw error;
|
|
19780
|
+
throw new ConnectedAccountBillingError(
|
|
19781
|
+
"Unable to update connected-account billing in Stripe.",
|
|
19782
|
+
"stripe_connection_billing_failed",
|
|
19783
|
+
503
|
|
19784
|
+
);
|
|
19785
|
+
}
|
|
19786
|
+
}
|
|
19787
|
+
async function currentConnectedAccountBillingView(userId, actualQuantity) {
|
|
19788
|
+
return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
|
|
19789
|
+
}
|
|
19790
|
+
|
|
19791
|
+
// src/api/stripe-routes.ts
|
|
19792
|
+
var stripe = new Stripe2(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
19560
19793
|
var stripeApp = new Hono19();
|
|
19561
19794
|
stripeApp.post("/webhooks", async (c) => {
|
|
19562
19795
|
const sig = c.req.header("stripe-signature");
|
|
@@ -19573,15 +19806,23 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
19573
19806
|
const invoice = event.data.object;
|
|
19574
19807
|
const memLineId = invoice.lines.data.map(linePriceId).find((id) => id && id in MEMORY_PLANS);
|
|
19575
19808
|
if (memLineId) return c.json({ received: true });
|
|
19576
|
-
const
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
19580
|
-
|
|
19581
|
-
|
|
19582
|
-
|
|
19583
|
-
|
|
19584
|
-
|
|
19809
|
+
const invoiceHasTierLine = invoice.lines.data.some((l) => {
|
|
19810
|
+
const id = linePriceId(l);
|
|
19811
|
+
return id && id in SUBSCRIPTION_TIERS;
|
|
19812
|
+
});
|
|
19813
|
+
if (invoiceHasTierLine) {
|
|
19814
|
+
const subId = invoice.subscription;
|
|
19815
|
+
const liveBasePlanPriceId = subId ? findBasePlanItem(await stripe.subscriptions.retrieve(subId))?.price?.id : void 0;
|
|
19816
|
+
const tierPriceId = liveBasePlanPriceId ?? invoice.lines.data.map(linePriceId).find((id) => id && id in SUBSCRIPTION_TIERS);
|
|
19817
|
+
const tier = tierPriceId ? SUBSCRIPTION_TIERS[tierPriceId] : void 0;
|
|
19818
|
+
if (tier) {
|
|
19819
|
+
const user = await resolveUser2(invoice.customer, invoice.customer_email ?? void 0);
|
|
19820
|
+
if (user && invoice.id && !await ledgerExistsForStripePI(invoice.id)) {
|
|
19821
|
+
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id);
|
|
19822
|
+
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
19823
|
+
const credentials = await syncScheduledActionCredentials(user);
|
|
19824
|
+
if (!credentials.ok) console.warn("[stripe] scheduled-action credential sync failed:", credentials.error ?? "unknown error");
|
|
19825
|
+
}
|
|
19585
19826
|
}
|
|
19586
19827
|
}
|
|
19587
19828
|
}
|
|
@@ -22832,8 +23073,8 @@ async function reconcileDiscoveredNangoConnections(identity, discovered) {
|
|
|
22832
23073
|
sql: `
|
|
22833
23074
|
INSERT INTO service_connections (
|
|
22834
23075
|
id, user_id, provider_config_key, provider, transport, upstream_connection_id,
|
|
22835
|
-
label, lifecycle_status, reconnect_required, legacy_owner_identity, created_at, updated_at
|
|
22836
|
-
) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
|
|
23076
|
+
label, lifecycle_status, reconnect_required, actions_enabled, legacy_owner_identity, created_at, updated_at
|
|
23077
|
+
) VALUES (?, ?, ?, ?, 'nango', ?, ?, ?, ?, 1, ?, COALESCE(?, datetime('now')), COALESCE(?, datetime('now')))
|
|
22837
23078
|
ON CONFLICT(user_id, provider_config_key, upstream_connection_id) DO UPDATE SET
|
|
22838
23079
|
provider = excluded.provider,
|
|
22839
23080
|
label = COALESCE(excluded.label, service_connections.label),
|
|
@@ -24902,217 +25143,6 @@ async function verifySchedulerIntegrationRequest(request, rawBody) {
|
|
|
24902
25143
|
return { requestId };
|
|
24903
25144
|
}
|
|
24904
25145
|
|
|
24905
|
-
// src/api/connected-account-billing.ts
|
|
24906
|
-
import Stripe2 from "stripe";
|
|
24907
|
-
var DEFAULT_CONNECTED_ACCOUNT_PRICE_ID = "price_1TtYPAS8aAcsk3TGHOgrDZiQ";
|
|
24908
|
-
var STRIPE_API_VERSION = "2026-02-25.clover";
|
|
24909
|
-
async function ensureConnectedAccountBillingSchema() {
|
|
24910
|
-
await getDb().execute(`CREATE TABLE IF NOT EXISTS connected_account_billing (
|
|
24911
|
-
user_id INTEGER PRIMARY KEY,
|
|
24912
|
-
stripe_subscription_id TEXT,
|
|
24913
|
-
stripe_subscription_item_id TEXT,
|
|
24914
|
-
price_id TEXT,
|
|
24915
|
-
quantity INTEGER NOT NULL DEFAULT 0,
|
|
24916
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
24917
|
-
last_error_code TEXT,
|
|
24918
|
-
synced_at TEXT,
|
|
24919
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
24920
|
-
)`);
|
|
24921
|
-
}
|
|
24922
|
-
async function getConnectedAccountBillingState(userId) {
|
|
24923
|
-
await ensureConnectedAccountBillingSchema();
|
|
24924
|
-
const result = await getDb().execute({
|
|
24925
|
-
sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
|
|
24926
|
-
args: [Number(userId)]
|
|
24927
|
-
});
|
|
24928
|
-
return result.rows[0] ? result.rows[0] : null;
|
|
24929
|
-
}
|
|
24930
|
-
async function setConnectedAccountBillingState(input) {
|
|
24931
|
-
await ensureConnectedAccountBillingSchema();
|
|
24932
|
-
await getDb().execute({
|
|
24933
|
-
sql: `INSERT INTO connected_account_billing
|
|
24934
|
-
(user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, synced_at, updated_at)
|
|
24935
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
|
|
24936
|
-
ON CONFLICT(user_id) DO UPDATE SET
|
|
24937
|
-
stripe_subscription_id = excluded.stripe_subscription_id,
|
|
24938
|
-
stripe_subscription_item_id = excluded.stripe_subscription_item_id,
|
|
24939
|
-
price_id = excluded.price_id,
|
|
24940
|
-
quantity = excluded.quantity,
|
|
24941
|
-
status = excluded.status,
|
|
24942
|
-
last_error_code = excluded.last_error_code,
|
|
24943
|
-
synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
|
|
24944
|
-
updated_at = datetime('now')`,
|
|
24945
|
-
args: [
|
|
24946
|
-
Number(input.userId),
|
|
24947
|
-
input.stripeSubscriptionId,
|
|
24948
|
-
input.stripeSubscriptionItemId,
|
|
24949
|
-
input.priceId,
|
|
24950
|
-
input.quantity,
|
|
24951
|
-
input.status,
|
|
24952
|
-
input.lastErrorCode,
|
|
24953
|
-
input.synced ? 1 : 0,
|
|
24954
|
-
input.synced ? 1 : 0
|
|
24955
|
-
]
|
|
24956
|
-
});
|
|
24957
|
-
const state = await getConnectedAccountBillingState(input.userId);
|
|
24958
|
-
if (!state) throw new Error("connected account billing state was not persisted");
|
|
24959
|
-
return state;
|
|
24960
|
-
}
|
|
24961
|
-
var ConnectedAccountBillingError = class extends Error {
|
|
24962
|
-
constructor(message, code, status = 409) {
|
|
24963
|
-
super(message);
|
|
24964
|
-
this.code = code;
|
|
24965
|
-
this.status = status;
|
|
24966
|
-
this.name = "ConnectedAccountBillingError";
|
|
24967
|
-
}
|
|
24968
|
-
code;
|
|
24969
|
-
status;
|
|
24970
|
-
};
|
|
24971
|
-
function connectedAccountPriceId() {
|
|
24972
|
-
const configured = process.env.CONNECTED_ACCOUNT_PRICE_ID?.trim();
|
|
24973
|
-
if (configured) return configured;
|
|
24974
|
-
return DEFAULT_CONNECTED_ACCOUNT_PRICE_ID || null;
|
|
24975
|
-
}
|
|
24976
|
-
function connectedAccountBillingView(actualQuantity, state) {
|
|
24977
|
-
const quantity = Math.max(0, Math.round(actualQuantity));
|
|
24978
|
-
return {
|
|
24979
|
-
provider: "nango",
|
|
24980
|
-
billingMode: "flat_recurring_usd_plus_credits",
|
|
24981
|
-
priceId: connectedAccountPriceId(),
|
|
24982
|
-
unitAmountUsd: CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
24983
|
-
interval: "month",
|
|
24984
|
-
quantity,
|
|
24985
|
-
projectedMonthlyUsd: quantity * CONNECTED_ACTIVE_CONNECTION_MONTHLY_USD,
|
|
24986
|
-
status: state?.status ?? "pending",
|
|
24987
|
-
lastErrorCode: state?.last_error_code ?? null,
|
|
24988
|
-
syncedAt: state?.synced_at ?? null,
|
|
24989
|
-
usage: CONNECTED_USAGE_RATE_POLICY.usage
|
|
24990
|
-
};
|
|
24991
|
-
}
|
|
24992
|
-
function findBasePlanItem(subscription) {
|
|
24993
|
-
return subscription.items.data.find((item) => !!item.price?.id && item.price.id in SUBSCRIPTION_TIERS);
|
|
24994
|
-
}
|
|
24995
|
-
function findConnectedAccountItem(subscription, priceId = connectedAccountPriceId()) {
|
|
24996
|
-
return priceId ? subscription.items.data.find((item) => item.price?.id === priceId) : void 0;
|
|
24997
|
-
}
|
|
24998
|
-
function liveSubscription(subscription) {
|
|
24999
|
-
return subscription.status === "active" || subscription.status === "trialing";
|
|
25000
|
-
}
|
|
25001
|
-
function safeStripeErrorCode(error) {
|
|
25002
|
-
if (error instanceof ConnectedAccountBillingError) return error.code;
|
|
25003
|
-
return "stripe_connection_billing_failed";
|
|
25004
|
-
}
|
|
25005
|
-
async function persistFailure(user, actualQuantity, error, subscriptionItemId = null) {
|
|
25006
|
-
await setConnectedAccountBillingState({
|
|
25007
|
-
userId: user.id,
|
|
25008
|
-
stripeSubscriptionId: user.subscription_id,
|
|
25009
|
-
stripeSubscriptionItemId: subscriptionItemId,
|
|
25010
|
-
priceId: connectedAccountPriceId(),
|
|
25011
|
-
quantity: actualQuantity,
|
|
25012
|
-
status: error instanceof ConnectedAccountBillingError && error.code !== "stripe_connection_billing_failed" ? "blocked" : "error",
|
|
25013
|
-
lastErrorCode: safeStripeErrorCode(error),
|
|
25014
|
-
synced: false
|
|
25015
|
-
});
|
|
25016
|
-
}
|
|
25017
|
-
async function reconcileConnectedAccountBilling(user, actualQuantity, stripeClient) {
|
|
25018
|
-
if (!Number.isSafeInteger(actualQuantity) || actualQuantity < 0) {
|
|
25019
|
-
throw new Error("actualQuantity must be a non-negative safe integer");
|
|
25020
|
-
}
|
|
25021
|
-
const priceId = connectedAccountPriceId();
|
|
25022
|
-
if (!priceId) {
|
|
25023
|
-
const error = new ConnectedAccountBillingError(
|
|
25024
|
-
"Connected-account billing is not configured.",
|
|
25025
|
-
"connected_account_price_not_configured",
|
|
25026
|
-
503
|
|
25027
|
-
);
|
|
25028
|
-
await persistFailure(user, actualQuantity, error);
|
|
25029
|
-
throw error;
|
|
25030
|
-
}
|
|
25031
|
-
if (!user.subscription_id) {
|
|
25032
|
-
if (actualQuantity === 0) {
|
|
25033
|
-
const state = await setConnectedAccountBillingState({
|
|
25034
|
-
userId: user.id,
|
|
25035
|
-
stripeSubscriptionId: null,
|
|
25036
|
-
stripeSubscriptionItemId: null,
|
|
25037
|
-
priceId,
|
|
25038
|
-
quantity: 0,
|
|
25039
|
-
status: "synced",
|
|
25040
|
-
lastErrorCode: null,
|
|
25041
|
-
synced: true
|
|
25042
|
-
});
|
|
25043
|
-
return connectedAccountBillingView(0, state);
|
|
25044
|
-
}
|
|
25045
|
-
const error = new ConnectedAccountBillingError(
|
|
25046
|
-
"An active paid plan is required before connected accounts can be billed.",
|
|
25047
|
-
"paid_plan_required",
|
|
25048
|
-
403
|
|
25049
|
-
);
|
|
25050
|
-
await persistFailure(user, actualQuantity, error);
|
|
25051
|
-
throw error;
|
|
25052
|
-
}
|
|
25053
|
-
const client2 = stripeClient ?? (() => {
|
|
25054
|
-
const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
|
|
25055
|
-
if (!secret2) {
|
|
25056
|
-
throw new ConnectedAccountBillingError(
|
|
25057
|
-
"Stripe is not configured.",
|
|
25058
|
-
"connected_account_price_not_configured",
|
|
25059
|
-
503
|
|
25060
|
-
);
|
|
25061
|
-
}
|
|
25062
|
-
return new Stripe2(secret2, { apiVersion: STRIPE_API_VERSION });
|
|
25063
|
-
})();
|
|
25064
|
-
let currentItem;
|
|
25065
|
-
try {
|
|
25066
|
-
const subscription = await client2.subscriptions.retrieve(user.subscription_id);
|
|
25067
|
-
currentItem = findConnectedAccountItem(subscription, priceId);
|
|
25068
|
-
if (!liveSubscription(subscription)) {
|
|
25069
|
-
throw new ConnectedAccountBillingError(
|
|
25070
|
-
"The plan subscription is not active.",
|
|
25071
|
-
"subscription_not_active",
|
|
25072
|
-
402
|
|
25073
|
-
);
|
|
25074
|
-
}
|
|
25075
|
-
if (!findBasePlanItem(subscription)) {
|
|
25076
|
-
throw new ConnectedAccountBillingError(
|
|
25077
|
-
"The base plan item could not be found on this subscription.",
|
|
25078
|
-
"subscription_plan_item_missing",
|
|
25079
|
-
409
|
|
25080
|
-
);
|
|
25081
|
-
}
|
|
25082
|
-
const currentQuantity = Math.max(0, currentItem?.quantity ?? 0);
|
|
25083
|
-
let resultingItem = currentItem;
|
|
25084
|
-
if (actualQuantity !== currentQuantity) {
|
|
25085
|
-
const updated = await client2.subscriptions.update(subscription.id, {
|
|
25086
|
-
items: actualQuantity === 0 ? currentItem ? [{ id: currentItem.id, deleted: true }] : [] : currentItem ? [{ id: currentItem.id, quantity: actualQuantity }] : [{ price: priceId, quantity: actualQuantity }],
|
|
25087
|
-
proration_behavior: "create_prorations"
|
|
25088
|
-
});
|
|
25089
|
-
resultingItem = findConnectedAccountItem(updated, priceId);
|
|
25090
|
-
}
|
|
25091
|
-
const state = await setConnectedAccountBillingState({
|
|
25092
|
-
userId: user.id,
|
|
25093
|
-
stripeSubscriptionId: subscription.id,
|
|
25094
|
-
stripeSubscriptionItemId: resultingItem?.id ?? null,
|
|
25095
|
-
priceId,
|
|
25096
|
-
quantity: actualQuantity,
|
|
25097
|
-
status: "synced",
|
|
25098
|
-
lastErrorCode: null,
|
|
25099
|
-
synced: true
|
|
25100
|
-
});
|
|
25101
|
-
return connectedAccountBillingView(actualQuantity, state);
|
|
25102
|
-
} catch (error) {
|
|
25103
|
-
await persistFailure(user, actualQuantity, error, currentItem?.id ?? null);
|
|
25104
|
-
if (error instanceof ConnectedAccountBillingError) throw error;
|
|
25105
|
-
throw new ConnectedAccountBillingError(
|
|
25106
|
-
"Unable to update connected-account billing in Stripe.",
|
|
25107
|
-
"stripe_connection_billing_failed",
|
|
25108
|
-
503
|
|
25109
|
-
);
|
|
25110
|
-
}
|
|
25111
|
-
}
|
|
25112
|
-
async function currentConnectedAccountBillingView(userId, actualQuantity) {
|
|
25113
|
-
return connectedAccountBillingView(actualQuantity, await getConnectedAccountBillingState(userId));
|
|
25114
|
-
}
|
|
25115
|
-
|
|
25116
25146
|
// src/api/server.ts
|
|
25117
25147
|
var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
25118
25148
|
var isProduction2 = secureCookies2;
|
|
@@ -26097,7 +26127,7 @@ app.post("/schedule-connections/actions/slack/send-message", auth2, requireInteg
|
|
|
26097
26127
|
user.email,
|
|
26098
26128
|
connectionId,
|
|
26099
26129
|
{ channel: body.channel, text: body.text },
|
|
26100
|
-
|
|
26130
|
+
"send-message",
|
|
26101
26131
|
connectedActionIdempotencyKey(c)
|
|
26102
26132
|
);
|
|
26103
26133
|
return c.json({ ok: true, result });
|
|
@@ -26117,7 +26147,7 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireInteg
|
|
|
26117
26147
|
user.email,
|
|
26118
26148
|
connectionId,
|
|
26119
26149
|
{ to: body.to, subject: body.subject, body: body.body },
|
|
26120
|
-
|
|
26150
|
+
"send-message",
|
|
26121
26151
|
connectedActionIdempotencyKey(c)
|
|
26122
26152
|
);
|
|
26123
26153
|
return c.json({ ok: true, result });
|
|
@@ -26125,6 +26155,76 @@ app.post("/schedule-connections/actions/gmail/send-message", auth2, requireInteg
|
|
|
26125
26155
|
return scheduleConnectionError(c, err, "Unable to send the email.");
|
|
26126
26156
|
}
|
|
26127
26157
|
});
|
|
26158
|
+
function toolResultText(result) {
|
|
26159
|
+
if (!result || typeof result !== "object") return null;
|
|
26160
|
+
const content = result.content;
|
|
26161
|
+
if (!Array.isArray(content)) return null;
|
|
26162
|
+
const textItem = content.find((item) => item && typeof item === "object" && item.type === "text");
|
|
26163
|
+
const text2 = textItem?.text;
|
|
26164
|
+
return typeof text2 === "string" ? text2 : null;
|
|
26165
|
+
}
|
|
26166
|
+
function parseEmailAddress(headerValue) {
|
|
26167
|
+
const value = headerValue?.trim();
|
|
26168
|
+
if (!value) return null;
|
|
26169
|
+
const angled = value.match(/^"?([^"<]*?)"?\s*<([^>]+)>$/);
|
|
26170
|
+
if (angled) return { email: angled[2].trim().toLowerCase(), name: angled[1].trim() || null };
|
|
26171
|
+
const bare = value.match(/^([^\s<>]+@[^\s<>]+)$/);
|
|
26172
|
+
return bare ? { email: bare[1].trim().toLowerCase(), name: null } : null;
|
|
26173
|
+
}
|
|
26174
|
+
app.post("/schedule-connections/actions/gmail/search-contacts", auth2, requireIntegrationsTier, async (c) => {
|
|
26175
|
+
const user = c.get("user");
|
|
26176
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26177
|
+
const connectionId = providerConfigKeyFrom(body.connectionId);
|
|
26178
|
+
const query = typeof body.query === "string" ? body.query.trim() : "";
|
|
26179
|
+
const maxMessages = Math.min(150, Math.max(1, Number.isFinite(body.maxMessages) ? Number(body.maxMessages) : 50));
|
|
26180
|
+
const pageToken = typeof body.pageToken === "string" ? body.pageToken : void 0;
|
|
26181
|
+
if (!connectionId || !query) return c.json({ ok: false, error: "connectionId and query are required." }, 400);
|
|
26182
|
+
try {
|
|
26183
|
+
const listRaw = await callScheduleConnectionRead(user.email, connectionId, "list-messages", { q: query, maxResults: maxMessages, pageToken });
|
|
26184
|
+
const listText = toolResultText(listRaw);
|
|
26185
|
+
const list = listText ? JSON.parse(listText) : {};
|
|
26186
|
+
const ids = (list.messages ?? []).map((m) => m.id);
|
|
26187
|
+
const messages = [];
|
|
26188
|
+
const contacts = /* @__PURE__ */ new Map();
|
|
26189
|
+
for (const id of ids) {
|
|
26190
|
+
const msgRaw = await callScheduleConnectionRead(user.email, connectionId, "get-message", { id, format: "metadata" });
|
|
26191
|
+
const msgText = toolResultText(msgRaw);
|
|
26192
|
+
if (!msgText) continue;
|
|
26193
|
+
const msg = JSON.parse(msgText);
|
|
26194
|
+
const headers = new Map((msg.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
26195
|
+
const date = headers.get("date") ?? null;
|
|
26196
|
+
const fromRaw = headers.get("from");
|
|
26197
|
+
const from = parseEmailAddress(fromRaw);
|
|
26198
|
+
const subject = headers.get("subject") ?? null;
|
|
26199
|
+
messages.push({ id, date, from: fromRaw ?? null, to: headers.get("to") ?? null, subject, snippet: msg.snippet ?? null });
|
|
26200
|
+
if (from) {
|
|
26201
|
+
const domain = from.email.split("@")[1] ?? null;
|
|
26202
|
+
const existing = contacts.get(from.email);
|
|
26203
|
+
if (existing) {
|
|
26204
|
+
existing.messageCount += 1;
|
|
26205
|
+
existing.name = existing.name ?? from.name;
|
|
26206
|
+
if (subject && existing.sampleSubjects.length < 5 && !existing.sampleSubjects.includes(subject)) existing.sampleSubjects.push(subject);
|
|
26207
|
+
if (date && (!existing.lastSeen || date > existing.lastSeen)) existing.lastSeen = date;
|
|
26208
|
+
if (date && (!existing.firstSeen || date < existing.firstSeen)) existing.firstSeen = date;
|
|
26209
|
+
} else {
|
|
26210
|
+
contacts.set(from.email, { email: from.email, name: from.name, domain, messageCount: 1, firstSeen: date, lastSeen: date, sampleSubjects: subject ? [subject] : [] });
|
|
26211
|
+
}
|
|
26212
|
+
}
|
|
26213
|
+
}
|
|
26214
|
+
const totalMatches = list.resultSizeEstimate;
|
|
26215
|
+
return c.json({
|
|
26216
|
+
ok: true,
|
|
26217
|
+
totalMatches,
|
|
26218
|
+
messagesFetched: messages.length,
|
|
26219
|
+
truncated: typeof totalMatches === "number" ? totalMatches > messages.length : Boolean(list.nextPageToken),
|
|
26220
|
+
nextPageToken: list.nextPageToken ?? null,
|
|
26221
|
+
messages,
|
|
26222
|
+
contacts: Array.from(contacts.values())
|
|
26223
|
+
});
|
|
26224
|
+
} catch (err) {
|
|
26225
|
+
return scheduleConnectionError(c, err, "Unable to search Gmail contacts.");
|
|
26226
|
+
}
|
|
26227
|
+
});
|
|
26128
26228
|
app.post("/schedule-connections/actions/google-calendar/create-event", auth2, requireIntegrationsTier, async (c) => {
|
|
26129
26229
|
const user = c.get("user");
|
|
26130
26230
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -26143,7 +26243,7 @@ app.post("/schedule-connections/actions/google-calendar/create-event", auth2, re
|
|
|
26143
26243
|
start: { dateTime: body.startDateTime, timeZone },
|
|
26144
26244
|
end: { dateTime: body.endDateTime, timeZone },
|
|
26145
26245
|
attendees
|
|
26146
|
-
},
|
|
26246
|
+
}, "create-event", connectedActionIdempotencyKey(c));
|
|
26147
26247
|
return c.json({ ok: true, result });
|
|
26148
26248
|
} catch (err) {
|
|
26149
26249
|
return scheduleConnectionError(c, err, "Unable to create the calendar event.");
|
|
@@ -26163,7 +26263,7 @@ app.post("/schedule-connections/actions/zoom/create-meeting", auth2, requireInte
|
|
|
26163
26263
|
durationMinutes: typeof body.durationMinutes === "number" ? body.durationMinutes : 30,
|
|
26164
26264
|
timezone: typeof body.timezone === "string" ? body.timezone : void 0,
|
|
26165
26265
|
agenda: body.agenda
|
|
26166
|
-
},
|
|
26266
|
+
}, "create-meeting", connectedActionIdempotencyKey(c));
|
|
26167
26267
|
return c.json({ ok: true, result });
|
|
26168
26268
|
} catch (err) {
|
|
26169
26269
|
return scheduleConnectionError(c, err, "Unable to create the Zoom meeting.");
|
|
@@ -27191,6 +27291,22 @@ app.post("/billing/concurrency/checkout", requireAllowedOrigin, sessionAuth, asy
|
|
|
27191
27291
|
return c.json({ error: message }, 500);
|
|
27192
27292
|
}
|
|
27193
27293
|
});
|
|
27294
|
+
async function chargeTierChangeNow(stripeClient, subscriptionId, customerId) {
|
|
27295
|
+
try {
|
|
27296
|
+
const draft = await stripeClient.invoices.create({
|
|
27297
|
+
customer: customerId,
|
|
27298
|
+
subscription: subscriptionId,
|
|
27299
|
+
auto_advance: true,
|
|
27300
|
+
collection_method: "charge_automatically"
|
|
27301
|
+
});
|
|
27302
|
+
const finalized = await stripeClient.invoices.finalizeInvoice(draft.id);
|
|
27303
|
+
if (finalized.amount_due <= 0 || finalized.status === "paid") return { ok: true, amountDue: finalized.amount_due };
|
|
27304
|
+
const paid = await stripeClient.invoices.pay(finalized.id);
|
|
27305
|
+
return { ok: paid.status === "paid", amountDue: paid.amount_due };
|
|
27306
|
+
} catch (err) {
|
|
27307
|
+
return { ok: false, amountDue: 0, error: err instanceof Error ? err.message : "Unable to charge the plan change immediately." };
|
|
27308
|
+
}
|
|
27309
|
+
}
|
|
27194
27310
|
app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
27195
27311
|
try {
|
|
27196
27312
|
const user = c.get("sessionUser");
|
|
@@ -27214,7 +27330,16 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
27214
27330
|
items: [{ id: itemId, price: tier.price_id }],
|
|
27215
27331
|
proration_behavior: "create_prorations"
|
|
27216
27332
|
});
|
|
27217
|
-
|
|
27333
|
+
const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
|
|
27334
|
+
if (!billed.ok) {
|
|
27335
|
+
return c.json({
|
|
27336
|
+
updated: true,
|
|
27337
|
+
tier: tier.tier,
|
|
27338
|
+
billed: false,
|
|
27339
|
+
error: billed.error ?? "Plan changed, but the prorated charge could not be collected. Update your payment method and it will retry automatically."
|
|
27340
|
+
});
|
|
27341
|
+
}
|
|
27342
|
+
return c.json({ updated: true, tier: tier.tier, billed: true });
|
|
27218
27343
|
}
|
|
27219
27344
|
}
|
|
27220
27345
|
const session = await stripeClient.checkout.sessions.create({
|
|
@@ -27331,7 +27456,16 @@ app.post("/billing/subscribe/terminal-checkout", auth2, async (c) => {
|
|
|
27331
27456
|
items: [{ id: itemId, price: tier.price_id }],
|
|
27332
27457
|
proration_behavior: "create_prorations"
|
|
27333
27458
|
});
|
|
27334
|
-
|
|
27459
|
+
const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
|
|
27460
|
+
if (!billed.ok) {
|
|
27461
|
+
return c.json({
|
|
27462
|
+
updated: true,
|
|
27463
|
+
tier: tier.tier,
|
|
27464
|
+
billed: false,
|
|
27465
|
+
message: `Switched to ${tier.label} (prorated), but the prorated charge could not be collected: ${billed.error ?? "unknown error"}. Update your payment method and it will retry automatically.`
|
|
27466
|
+
});
|
|
27467
|
+
}
|
|
27468
|
+
return c.json({ updated: true, tier: tier.tier, billed: true, message: `Switched to ${tier.label} and charged the prorated difference immediately \u2014 credits and concurrency are live now.` });
|
|
27335
27469
|
}
|
|
27336
27470
|
}
|
|
27337
27471
|
const session = await stripeClient.checkout.sessions.create({
|
|
@@ -27633,4 +27767,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
27633
27767
|
export {
|
|
27634
27768
|
app
|
|
27635
27769
|
};
|
|
27636
|
-
//# sourceMappingURL=server-
|
|
27770
|
+
//# sourceMappingURL=server-NHLEIKDZ.js.map
|