feat: fixes and formatter
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
const { onRequest } = require("firebase-functions/v2/https");
|
||||
const { HttpsError } = require("firebase-functions/https");
|
||||
const { onRequest } = require('firebase-functions/v2/https')
|
||||
const { HttpsError } = require('firebase-functions/https')
|
||||
|
||||
const { ORDER_TYPES, createOrderDocument } = require("../helpers/orders");
|
||||
const { getStripeClient } = require("../../helpers/stripe");
|
||||
const { REGION } = require("./config");
|
||||
const { ORDER_TYPES, createOrderDocument } = require('../helpers/orders')
|
||||
const { getStripeClient } = require('../../helpers/stripe')
|
||||
const { REGION } = require('./config')
|
||||
const {
|
||||
paymentsCollection,
|
||||
resolveStripeWebhookSecret,
|
||||
@@ -17,25 +17,21 @@ const {
|
||||
buildSubscriptionPayload,
|
||||
resolveUserContext,
|
||||
upsertPaymentDocument,
|
||||
} = require("./shared");
|
||||
const { PREMIUM_SUBSCRIPTION_STATUSES } = require("./constants");
|
||||
} = require('./shared')
|
||||
const { PREMIUM_SUBSCRIPTION_STATUSES } = require('./constants')
|
||||
|
||||
const handleCheckoutSessionCompleted = async (
|
||||
session,
|
||||
event,
|
||||
{ stripe } = {},
|
||||
) => {
|
||||
if (!session || typeof session !== "object") {
|
||||
return;
|
||||
const handleCheckoutSessionCompleted = async (session, event, { stripe } = {}) => {
|
||||
if (!session || typeof session !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const firebaseUid = extractFirebaseUid(session.metadata);
|
||||
const firebaseUid = extractFirebaseUid(session.metadata)
|
||||
const paymentDocRef = await upsertPaymentDocument(session.id, {
|
||||
userId: firebaseUid || null,
|
||||
customerId: session.customer || null,
|
||||
subscriptionId: session.subscription || null,
|
||||
invoiceId: session.invoice || null,
|
||||
status: session.status || "completed",
|
||||
status: session.status || 'completed',
|
||||
paymentStatus: session.payment_status || null,
|
||||
mode: session.mode || null,
|
||||
amountSubtotal: session.amount_subtotal ?? null,
|
||||
@@ -44,109 +40,103 @@ const handleCheckoutSessionCompleted = async (
|
||||
metadata: session.metadata || {},
|
||||
completedAt: toFirestoreTimestamp(session.created),
|
||||
expiresAt: toFirestoreTimestamp(session.expires_at),
|
||||
paymentIntentId:
|
||||
typeof session.payment_intent === "string"
|
||||
? session.payment_intent
|
||||
: null,
|
||||
paymentIntentId: typeof session.payment_intent === 'string' ? session.payment_intent : null,
|
||||
lastEventType: event?.type || null,
|
||||
lastEventId: event?.id || null,
|
||||
lastEventAt: getServerTimestamp(),
|
||||
});
|
||||
})
|
||||
|
||||
const { uid, userRef } = await resolveUserContext({
|
||||
metadata: session.metadata,
|
||||
customerId: session.customer,
|
||||
});
|
||||
})
|
||||
|
||||
if (userRef) {
|
||||
const lastEvent = buildEventSnapshot(event?.type, session.id);
|
||||
const lastEvent = buildEventSnapshot(event?.type, session.id)
|
||||
if (firebaseUid) {
|
||||
lastEvent.uid = firebaseUid;
|
||||
lastEvent.uid = firebaseUid
|
||||
}
|
||||
|
||||
const userUpdate = {
|
||||
lastStripeWebhookEvent: lastEvent,
|
||||
};
|
||||
}
|
||||
|
||||
if (session.customer) {
|
||||
userUpdate.stripeCustomerId = session.customer;
|
||||
userUpdate.stripeCustomerId = session.customer
|
||||
}
|
||||
|
||||
if (session.metadata?.subscriptionLevel) {
|
||||
userUpdate.premiumLevel = session.metadata.subscriptionLevel;
|
||||
userUpdate.premiumLevel = session.metadata.subscriptionLevel
|
||||
}
|
||||
|
||||
if (session.metadata?.subscriptionBillingPeriod) {
|
||||
userUpdate.premiumBillingPeriod =
|
||||
session.metadata.subscriptionBillingPeriod;
|
||||
userUpdate.premiumBillingPeriod = session.metadata.subscriptionBillingPeriod
|
||||
}
|
||||
|
||||
await userRef.set(userUpdate, { merge: true });
|
||||
await userRef.set(userUpdate, { merge: true })
|
||||
}
|
||||
|
||||
if (
|
||||
stripe &&
|
||||
session.mode === "subscription" &&
|
||||
typeof session.subscription === "string" &&
|
||||
session.mode === 'subscription' &&
|
||||
typeof session.subscription === 'string' &&
|
||||
session.subscription
|
||||
) {
|
||||
try {
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription,
|
||||
{ expand: ["items.data.price.product"] },
|
||||
);
|
||||
const subscription = await stripe.subscriptions.retrieve(session.subscription, {
|
||||
expand: ['items.data.price.product'],
|
||||
})
|
||||
if (subscription) {
|
||||
await handleCustomerSubscriptionEvent(subscription, event, { stripe });
|
||||
await handleCustomerSubscriptionEvent(subscription, event, { stripe })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[subscription-handleCheckoutSessionCompleted] Unable to sync subscription",
|
||||
'[subscription-handleCheckoutSessionCompleted] Unable to sync subscription',
|
||||
session.subscription,
|
||||
error,
|
||||
);
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
session.mode === "payment" &&
|
||||
(session.payment_status === "paid" ||
|
||||
session.payment_status === "no_payment_required") &&
|
||||
session.metadata?.purchaseType === "COIN_PACK" &&
|
||||
session.mode === 'payment' &&
|
||||
(session.payment_status === 'paid' || session.payment_status === 'no_payment_required') &&
|
||||
session.metadata?.purchaseType === 'COIN_PACK' &&
|
||||
userRef
|
||||
) {
|
||||
const coinAmountRaw = Number(session.metadata?.coinAmount || 0);
|
||||
const coinAmount = Number.isFinite(coinAmountRaw) ? coinAmountRaw : 0;
|
||||
const coinAmountRaw = Number(session.metadata?.coinAmount || 0)
|
||||
const coinAmount = Number.isFinite(coinAmountRaw) ? coinAmountRaw : 0
|
||||
|
||||
if (coinAmount > 0 && paymentDocRef) {
|
||||
let paymentSnapshot = null;
|
||||
let paymentSnapshot = null
|
||||
try {
|
||||
paymentSnapshot = await paymentDocRef.get();
|
||||
paymentSnapshot = await paymentDocRef.get()
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[subscription-handleCheckoutSessionCompleted] Unable to read payment doc",
|
||||
'[subscription-handleCheckoutSessionCompleted] Unable to read payment doc',
|
||||
session.id,
|
||||
error?.message || error,
|
||||
);
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
|
||||
const alreadyGranted = Boolean(
|
||||
paymentSnapshot?.exists && paymentSnapshot.data()?.coinPackGrantedAt,
|
||||
);
|
||||
paymentSnapshot?.exists && paymentSnapshot.data()?.coinPackGrantedAt
|
||||
)
|
||||
|
||||
if (!alreadyGranted) {
|
||||
const targetUserId = uid || firebaseUid || userRef.id;
|
||||
const targetUserId = uid || firebaseUid || userRef.id
|
||||
|
||||
await createOrderDocument({
|
||||
userId: targetUserId,
|
||||
type: ORDER_TYPES.COINS,
|
||||
amount: coinAmount,
|
||||
metadata: {
|
||||
source: "STRIPE_CHECKOUT",
|
||||
source: 'STRIPE_CHECKOUT',
|
||||
paymentId: session.id || null,
|
||||
coinPackKey: session.metadata?.coinPackKey || null,
|
||||
},
|
||||
orderId: `stripe_${session.id}`,
|
||||
});
|
||||
})
|
||||
|
||||
await paymentDocRef.set(
|
||||
{
|
||||
@@ -154,25 +144,21 @@ const handleCheckoutSessionCompleted = async (
|
||||
coinPackGrantedAmount: coinAmount,
|
||||
coinPackGrantedKey: session.metadata?.coinPackKey || null,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleCustomerSubscriptionEvent = async (
|
||||
subscription,
|
||||
event,
|
||||
{ stripe } = {},
|
||||
) => {
|
||||
if (!subscription || typeof subscription !== "object") {
|
||||
return;
|
||||
const handleCustomerSubscriptionEvent = async (subscription, event, { stripe } = {}) => {
|
||||
if (!subscription || typeof subscription !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const subscriptionPayload = buildSubscriptionPayload(subscription);
|
||||
const subscriptionPayload = buildSubscriptionPayload(subscription)
|
||||
if (!subscriptionPayload) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -182,36 +168,36 @@ const handleCustomerSubscriptionEvent = async (
|
||||
} = await resolveUserContext({
|
||||
metadata: subscription.metadata,
|
||||
customerId: subscription.customer,
|
||||
});
|
||||
})
|
||||
|
||||
let userData = resolvedUserData || null;
|
||||
let userData = resolvedUserData || null
|
||||
if (!userData && userRef) {
|
||||
try {
|
||||
const snapshot = await userRef.get();
|
||||
userData = snapshot.exists ? snapshot.data() || null : null;
|
||||
const snapshot = await userRef.get()
|
||||
userData = snapshot.exists ? snapshot.data() || null : null
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[subscription-handleCustomerSubscriptionEvent] Unable to read user",
|
||||
'[subscription-handleCustomerSubscriptionEvent] Unable to read user',
|
||||
subscription.customer,
|
||||
error?.message || error,
|
||||
);
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedCustomerId = subscriptionPayload?.customerId || null;
|
||||
let resolvedCustomerId = subscriptionPayload?.customerId || null
|
||||
if (!resolvedCustomerId && subscription.customer) {
|
||||
resolvedCustomerId = subscription.customer;
|
||||
resolvedCustomerId = subscription.customer
|
||||
}
|
||||
|
||||
const fallbackUid = extractFirebaseUid(subscription.metadata);
|
||||
const resolvedUid = uid || fallbackUid || null;
|
||||
const fallbackUid = extractFirebaseUid(subscription.metadata)
|
||||
const resolvedUid = uid || fallbackUid || null
|
||||
|
||||
await upsertPaymentDocument(subscription.id, {
|
||||
userId: resolvedUid,
|
||||
customerId: resolvedCustomerId,
|
||||
subscriptionId: subscription.id || null,
|
||||
status: subscription.status || null,
|
||||
mode: "subscription",
|
||||
mode: 'subscription',
|
||||
priceId: subscriptionPayload?.priceId || null,
|
||||
productId: subscriptionPayload?.productId || null,
|
||||
cancelAtPeriodEnd: subscriptionPayload?.cancelAtPeriodEnd ?? null,
|
||||
@@ -224,76 +210,66 @@ const handleCustomerSubscriptionEvent = async (
|
||||
lastEventType: event?.type || null,
|
||||
lastEventId: event?.id || null,
|
||||
lastEventAt: getServerTimestamp(),
|
||||
});
|
||||
})
|
||||
|
||||
if (!userRef) {
|
||||
console.warn(
|
||||
"[subscription-handleCustomerSubscriptionEvent] User not resolved",
|
||||
{
|
||||
subscriptionId: subscription?.id || null,
|
||||
customerId: subscription?.customer || null,
|
||||
metadataKeys: Object.keys(subscription?.metadata || {}),
|
||||
eventType: event?.type || null,
|
||||
},
|
||||
);
|
||||
return;
|
||||
console.warn('[subscription-handleCustomerSubscriptionEvent] User not resolved', {
|
||||
subscriptionId: subscription?.id || null,
|
||||
customerId: subscription?.customer || null,
|
||||
metadataKeys: Object.keys(subscription?.metadata || {}),
|
||||
eventType: event?.type || null,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const lastEvent = buildEventSnapshot(event?.type, subscription.id);
|
||||
const lastEvent = buildEventSnapshot(event?.type, subscription.id)
|
||||
if (resolvedUid) {
|
||||
lastEvent.uid = resolvedUid;
|
||||
lastEvent.uid = resolvedUid
|
||||
}
|
||||
|
||||
const priceMeta = getSubscriptionMetaFromPrice(subscriptionPayload?.priceId);
|
||||
const metadataLevel = subscription.metadata?.subscriptionLevel || null;
|
||||
const metadataPeriod =
|
||||
subscription.metadata?.subscriptionBillingPeriod || null;
|
||||
const resolvedLevel = metadataLevel || priceMeta?.level || null;
|
||||
const resolvedPeriod = metadataPeriod || priceMeta?.billingPeriod || null;
|
||||
const priceMeta = getSubscriptionMetaFromPrice(subscriptionPayload?.priceId)
|
||||
const metadataLevel = subscription.metadata?.subscriptionLevel || null
|
||||
const metadataPeriod = subscription.metadata?.subscriptionBillingPeriod || null
|
||||
const resolvedLevel = metadataLevel || priceMeta?.level || null
|
||||
const resolvedPeriod = metadataPeriod || priceMeta?.billingPeriod || null
|
||||
|
||||
const isPremium = subscription.status
|
||||
? PREMIUM_SUBSCRIPTION_STATUSES.has(subscription.status)
|
||||
: false;
|
||||
: false
|
||||
|
||||
const primaryItem = Array.isArray(subscriptionPayload?.items)
|
||||
? subscriptionPayload.items[0]
|
||||
: null;
|
||||
: null
|
||||
|
||||
let coinsPerMonth = null;
|
||||
let coinsPerMonth = null
|
||||
const productMetadata =
|
||||
primaryItem?.price &&
|
||||
typeof primaryItem.price === "object" &&
|
||||
typeof primaryItem.price === 'object' &&
|
||||
primaryItem.price.product &&
|
||||
typeof primaryItem.price.product === "object"
|
||||
typeof primaryItem.price.product === 'object'
|
||||
? primaryItem.price.product.metadata
|
||||
: null;
|
||||
: null
|
||||
|
||||
coinsPerMonth = parseCoinsPerMonth(productMetadata || {});
|
||||
coinsPerMonth = parseCoinsPerMonth(productMetadata || {})
|
||||
|
||||
if (coinsPerMonth === null && stripe && primaryItem?.price?.id) {
|
||||
try {
|
||||
const priceWithProduct = await stripe.prices.retrieve(
|
||||
primaryItem.price.id,
|
||||
{ expand: ["product"] },
|
||||
);
|
||||
coinsPerMonth = parseCoinsPerMonth(
|
||||
priceWithProduct?.product?.metadata || {},
|
||||
);
|
||||
const priceWithProduct = await stripe.prices.retrieve(primaryItem.price.id, {
|
||||
expand: ['product'],
|
||||
})
|
||||
coinsPerMonth = parseCoinsPerMonth(priceWithProduct?.product?.metadata || {})
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[subscription-handleCustomerSubscriptionEvent] Unable to retrieve price product metadata",
|
||||
'[subscription-handleCustomerSubscriptionEvent] Unable to retrieve price product metadata',
|
||||
primaryItem.price.id,
|
||||
error?.message || error,
|
||||
);
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let subscriptionNextGrantAt = null;
|
||||
let subscriptionNextGrantAt = null
|
||||
if (coinsPerMonth !== null && subscriptionPayload?.currentPeriodEnd) {
|
||||
subscriptionNextGrantAt = computeNextGrantTimestamp(
|
||||
subscriptionPayload.currentPeriodEnd,
|
||||
1,
|
||||
);
|
||||
subscriptionNextGrantAt = computeNextGrantTimestamp(subscriptionPayload.currentPeriodEnd, 1)
|
||||
}
|
||||
|
||||
const userUpdate = {
|
||||
@@ -313,53 +289,46 @@ const handleCustomerSubscriptionEvent = async (
|
||||
premiumLevel: isPremium ? resolvedLevel : null,
|
||||
premiumBillingPeriod: isPremium ? resolvedPeriod : null,
|
||||
subscriptionNextGrantAt,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isPremium) {
|
||||
userUpdate.subscriptionNextGrantAt = null;
|
||||
userUpdate.subscriptionNextGrantAt = null
|
||||
}
|
||||
|
||||
await userRef.set(userUpdate, { merge: true });
|
||||
};
|
||||
await userRef.set(userUpdate, { merge: true })
|
||||
}
|
||||
|
||||
const handleInvoiceEvent = async (invoice, event, { stripe } = {}) => {
|
||||
if (!invoice || typeof invoice !== "object") {
|
||||
return;
|
||||
if (!invoice || typeof invoice !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const firebaseUid = extractFirebaseUid(invoice.metadata);
|
||||
const eventType = event?.type || null;
|
||||
const paymentDocRef = paymentsCollection.doc(invoice.id);
|
||||
const firebaseUid = extractFirebaseUid(invoice.metadata)
|
||||
const eventType = event?.type || null
|
||||
const paymentDocRef = paymentsCollection.doc(invoice.id)
|
||||
|
||||
let resolvedSubscriptionId =
|
||||
typeof invoice.subscription === "string" && invoice.subscription
|
||||
? invoice.subscription
|
||||
: null;
|
||||
typeof invoice.subscription === 'string' && invoice.subscription ? invoice.subscription : null
|
||||
|
||||
if (!resolvedSubscriptionId) {
|
||||
const lineSubscriptionId = Array.isArray(invoice?.lines?.data)
|
||||
? invoice.lines.data
|
||||
.map((line) =>
|
||||
typeof line?.subscription === "string" && line.subscription
|
||||
? line.subscription
|
||||
: null,
|
||||
typeof line?.subscription === 'string' && line.subscription ? line.subscription : null
|
||||
)
|
||||
.find((value) => value)
|
||||
: null;
|
||||
: null
|
||||
|
||||
if (lineSubscriptionId) {
|
||||
resolvedSubscriptionId = lineSubscriptionId;
|
||||
console.log(
|
||||
"[subscription-handleInvoiceEvent] Subscription resolved from invoice line",
|
||||
{
|
||||
invoiceId: invoice?.id || null,
|
||||
subscriptionId: resolvedSubscriptionId,
|
||||
},
|
||||
);
|
||||
resolvedSubscriptionId = lineSubscriptionId
|
||||
console.log('[subscription-handleInvoiceEvent] Subscription resolved from invoice line', {
|
||||
invoiceId: invoice?.id || null,
|
||||
subscriptionId: resolvedSubscriptionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[subscription-handleInvoiceEvent] Received invoice webhook", {
|
||||
console.log('[subscription-handleInvoiceEvent] Received invoice webhook', {
|
||||
eventType,
|
||||
invoiceId: invoice?.id || null,
|
||||
subscriptionId: invoice?.subscription || null,
|
||||
@@ -368,17 +337,14 @@ const handleInvoiceEvent = async (invoice, event, { stripe } = {}) => {
|
||||
status: invoice?.status || null,
|
||||
billingReason: invoice?.billing_reason || null,
|
||||
attemptCount: invoice?.attempt_count ?? null,
|
||||
});
|
||||
})
|
||||
|
||||
await upsertPaymentDocument(invoice.id, {
|
||||
userId: firebaseUid || null,
|
||||
customerId: invoice.customer || null,
|
||||
subscriptionId: resolvedSubscriptionId,
|
||||
status: invoice.status || null,
|
||||
paymentStatus:
|
||||
eventType === "invoice.payment_failed"
|
||||
? "failed"
|
||||
: invoice.status || null,
|
||||
paymentStatus: eventType === 'invoice.payment_failed' ? 'failed' : invoice.status || null,
|
||||
amountDue: invoice.amount_due ?? null,
|
||||
amountPaid: invoice.amount_paid ?? null,
|
||||
amountRemaining: invoice.amount_remaining ?? null,
|
||||
@@ -394,8 +360,8 @@ const handleInvoiceEvent = async (invoice, event, { stripe } = {}) => {
|
||||
lastEventType: eventType,
|
||||
lastEventId: event?.id || null,
|
||||
lastEventAt: getServerTimestamp(),
|
||||
mode: "invoice",
|
||||
});
|
||||
mode: 'invoice',
|
||||
})
|
||||
|
||||
const {
|
||||
uid,
|
||||
@@ -404,124 +370,109 @@ const handleInvoiceEvent = async (invoice, event, { stripe } = {}) => {
|
||||
} = await resolveUserContext({
|
||||
metadata: invoice.metadata,
|
||||
customerId: invoice.customer,
|
||||
});
|
||||
})
|
||||
|
||||
if (!userRef) {
|
||||
console.warn(
|
||||
"[subscription-handleInvoiceEvent] User context not resolved",
|
||||
{
|
||||
invoiceId: invoice?.id || null,
|
||||
customerId: invoice?.customer || null,
|
||||
firebaseUid: firebaseUid || null,
|
||||
metadataKeys: Object.keys(invoice?.metadata || {}),
|
||||
},
|
||||
);
|
||||
return;
|
||||
console.warn('[subscription-handleInvoiceEvent] User context not resolved', {
|
||||
invoiceId: invoice?.id || null,
|
||||
customerId: invoice?.customer || null,
|
||||
firebaseUid: firebaseUid || null,
|
||||
metadataKeys: Object.keys(invoice?.metadata || {}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let userData = resolvedUserData || null;
|
||||
let userData = resolvedUserData || null
|
||||
if (!userData) {
|
||||
try {
|
||||
const snapshot = await userRef.get();
|
||||
userData = snapshot.exists ? snapshot.data() || null : null;
|
||||
const snapshot = await userRef.get()
|
||||
userData = snapshot.exists ? snapshot.data() || null : null
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[subscription-handleInvoiceEvent] Unable to read user",
|
||||
'[subscription-handleInvoiceEvent] Unable to read user',
|
||||
invoice.customer,
|
||||
error?.message || error,
|
||||
);
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const lastEvent = buildEventSnapshot(eventType, invoice.id);
|
||||
const lastEvent = buildEventSnapshot(eventType, invoice.id)
|
||||
if (firebaseUid) {
|
||||
lastEvent.uid = firebaseUid;
|
||||
lastEvent.uid = firebaseUid
|
||||
}
|
||||
|
||||
const userUpdate = {
|
||||
lastStripeWebhookEvent: lastEvent,
|
||||
};
|
||||
}
|
||||
|
||||
const billingReason = invoice.billing_reason || null;
|
||||
const isInvoicePaid = invoice.status === "paid";
|
||||
if (
|
||||
billingReason === "subscription_create" ||
|
||||
billingReason === "subscription_cycle"
|
||||
) {
|
||||
const billingReason = invoice.billing_reason || null
|
||||
const isInvoicePaid = invoice.status === 'paid'
|
||||
if (billingReason === 'subscription_create' || billingReason === 'subscription_cycle') {
|
||||
if (isInvoicePaid && invoice.subscription) {
|
||||
userUpdate.subscriptionLastInvoiceAt = getServerTimestamp();
|
||||
userUpdate.subscriptionLastInvoiceAt = getServerTimestamp()
|
||||
}
|
||||
}
|
||||
|
||||
await userRef.set(userUpdate, { merge: true });
|
||||
await userRef.set(userUpdate, { merge: true })
|
||||
|
||||
const subscriptionLine = Array.isArray(invoice?.lines?.data)
|
||||
? invoice.lines.data.find(
|
||||
(line) =>
|
||||
line &&
|
||||
typeof line === "object" &&
|
||||
(line.type === "subscription" || line.price),
|
||||
(line) => line && typeof line === 'object' && (line.type === 'subscription' || line.price)
|
||||
)
|
||||
: null;
|
||||
: null
|
||||
|
||||
const priceId =
|
||||
typeof subscriptionLine?.price?.id === "string"
|
||||
typeof subscriptionLine?.price?.id === 'string'
|
||||
? subscriptionLine.price.id
|
||||
: typeof subscriptionLine?.price === "string"
|
||||
: typeof subscriptionLine?.price === 'string'
|
||||
? subscriptionLine.price
|
||||
: null;
|
||||
: null
|
||||
|
||||
const priceMeta = getSubscriptionMetaFromPrice(priceId) || {};
|
||||
const priceMeta = getSubscriptionMetaFromPrice(priceId) || {}
|
||||
const billingPeriod =
|
||||
priceMeta.billingPeriod ||
|
||||
(subscriptionLine?.price?.recurring?.interval === "year"
|
||||
? "annual"
|
||||
: subscriptionLine?.price?.recurring?.interval === "month"
|
||||
? "monthly"
|
||||
: null);
|
||||
(subscriptionLine?.price?.recurring?.interval === 'year'
|
||||
? 'annual'
|
||||
: subscriptionLine?.price?.recurring?.interval === 'month'
|
||||
? 'monthly'
|
||||
: null)
|
||||
|
||||
const coinsPerMonth =
|
||||
priceMeta.coinsPerMonth ??
|
||||
parseCoinsPerMonth(subscriptionLine?.price?.product?.metadata || {}) ??
|
||||
null;
|
||||
null
|
||||
|
||||
const coinsToGrant =
|
||||
billingPeriod === "annual" && typeof coinsPerMonth === "number"
|
||||
? coinsPerMonth * 12
|
||||
: null;
|
||||
billingPeriod === 'annual' && typeof coinsPerMonth === 'number' ? coinsPerMonth * 12 : null
|
||||
|
||||
const targetSubscriptionId =
|
||||
resolvedSubscriptionId ||
|
||||
(typeof invoice.subscription === "string" ? invoice.subscription : null) ||
|
||||
(typeof subscriptionLine?.subscription === "string"
|
||||
? subscriptionLine.subscription
|
||||
: null);
|
||||
(typeof invoice.subscription === 'string' ? invoice.subscription : null) ||
|
||||
(typeof subscriptionLine?.subscription === 'string' ? subscriptionLine.subscription : null)
|
||||
|
||||
const shouldGrantUpfront =
|
||||
isInvoicePaid &&
|
||||
coinsToGrant &&
|
||||
(billingReason === "subscription_create" ||
|
||||
billingReason === "subscription_cycle");
|
||||
(billingReason === 'subscription_create' || billingReason === 'subscription_cycle')
|
||||
|
||||
if (shouldGrantUpfront && targetSubscriptionId) {
|
||||
let grantSnapshot = null;
|
||||
let grantSnapshot = null
|
||||
try {
|
||||
grantSnapshot = await paymentDocRef.get();
|
||||
grantSnapshot = await paymentDocRef.get()
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[subscription-handleInvoiceEvent] Unable to read payment doc before grant",
|
||||
'[subscription-handleInvoiceEvent] Unable to read payment doc before grant',
|
||||
invoice?.id || null,
|
||||
error?.message || error,
|
||||
);
|
||||
error?.message || error
|
||||
)
|
||||
}
|
||||
|
||||
const alreadyGranted =
|
||||
grantSnapshot?.exists &&
|
||||
Boolean(grantSnapshot.data()?.subscriptionCoinsGrantedAt);
|
||||
grantSnapshot?.exists && Boolean(grantSnapshot.data()?.subscriptionCoinsGrantedAt)
|
||||
|
||||
if (!alreadyGranted) {
|
||||
const targetUserId = uid || firebaseUid || userRef.id;
|
||||
const orderId = `subscription_${targetSubscriptionId}_invoice_${invoice.id}`;
|
||||
const targetUserId = uid || firebaseUid || userRef.id
|
||||
const orderId = `subscription_${targetSubscriptionId}_invoice_${invoice.id}`
|
||||
|
||||
try {
|
||||
const { orderId: processedOrderId } = await createOrderDocument({
|
||||
@@ -529,139 +480,129 @@ const handleInvoiceEvent = async (invoice, event, { stripe } = {}) => {
|
||||
type: ORDER_TYPES.SUBSCRIPTION,
|
||||
amount: coinsToGrant,
|
||||
metadata: {
|
||||
source: "STRIPE_INVOICE",
|
||||
source: 'STRIPE_INVOICE',
|
||||
billingPeriod,
|
||||
coinsPerMonth,
|
||||
invoiceId: invoice.id || null,
|
||||
subscriptionId: targetSubscriptionId,
|
||||
grantStrategy: "upfront",
|
||||
grantStrategy: 'upfront',
|
||||
},
|
||||
orderId,
|
||||
});
|
||||
})
|
||||
|
||||
await paymentDocRef.set(
|
||||
{
|
||||
subscriptionCoinsGrantedAt: getServerTimestamp(),
|
||||
subscriptionCoinsGrantAmount: coinsToGrant,
|
||||
subscriptionCoinsGrantOrderId: processedOrderId,
|
||||
subscriptionCoinsGrantSource: "invoice_upfront",
|
||||
subscriptionCoinsGrantSource: 'invoice_upfront',
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
|
||||
await userRef.set(
|
||||
{
|
||||
subscriptionLastGrantAt: getServerTimestamp(),
|
||||
subscriptionLastGrantAmount: coinsToGrant,
|
||||
subscriptionLastGrantOrderId: processedOrderId,
|
||||
subscriptionLastGrantSource: "invoice_upfront",
|
||||
subscriptionLastGrantSource: 'invoice_upfront',
|
||||
subscriptionNextGrantAt: null,
|
||||
subscriptionGrantInterval: null,
|
||||
subscriptionGrantStrategy: "upfront",
|
||||
subscriptionGrantStrategy: 'upfront',
|
||||
subscriptionCoinsPerMonth: coinsPerMonth,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[subscription-handleInvoiceEvent] Unable to grant upfront subscription coins",
|
||||
'[subscription-handleInvoiceEvent] Unable to grant upfront subscription coins',
|
||||
{
|
||||
invoiceId: invoice?.id || null,
|
||||
subscriptionId: targetSubscriptionId,
|
||||
error: error?.message || error,
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleStripeWebhookEvent = async ({ event, stripe }) => {
|
||||
if (!event || typeof event !== "object") {
|
||||
return;
|
||||
if (!event || typeof event !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const eventType = event.type;
|
||||
const eventType = event.type
|
||||
switch (eventType) {
|
||||
case "checkout.session.completed":
|
||||
case 'checkout.session.completed':
|
||||
await handleCheckoutSessionCompleted(event.data?.object, event, {
|
||||
stripe,
|
||||
});
|
||||
break;
|
||||
case "customer.subscription.created":
|
||||
case "customer.subscription.updated":
|
||||
case "customer.subscription.deleted":
|
||||
})
|
||||
break
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated':
|
||||
case 'customer.subscription.deleted':
|
||||
await handleCustomerSubscriptionEvent(event.data?.object, event, {
|
||||
stripe,
|
||||
});
|
||||
break;
|
||||
case "invoice.payment_succeeded":
|
||||
case "invoice.payment_failed":
|
||||
case "invoice.finalized":
|
||||
await handleInvoiceEvent(event.data?.object, event, { stripe });
|
||||
break;
|
||||
})
|
||||
break
|
||||
case 'invoice.payment_succeeded':
|
||||
case 'invoice.payment_failed':
|
||||
case 'invoice.finalized':
|
||||
await handleInvoiceEvent(event.data?.object, event, { stripe })
|
||||
break
|
||||
default:
|
||||
console.log(
|
||||
"[subscription-handleStripeWebhookEvent] Unhandled event type",
|
||||
eventType,
|
||||
);
|
||||
console.log('[subscription-handleStripeWebhookEvent] Unhandled event type', eventType)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleStripeWebhook = onRequest({ region: REGION }, async (req, res) => {
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).send("Method Not Allowed");
|
||||
return;
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).send('Method Not Allowed')
|
||||
return
|
||||
}
|
||||
|
||||
const signature = req.headers["stripe-signature"];
|
||||
const signature = req.headers['stripe-signature']
|
||||
if (!signature) {
|
||||
res.status(400).send("Missing Stripe signature");
|
||||
return;
|
||||
res.status(400).send('Missing Stripe signature')
|
||||
return
|
||||
}
|
||||
|
||||
let rawBody = req.rawBody;
|
||||
let rawBody = req.rawBody
|
||||
if (!rawBody && req.body) {
|
||||
rawBody = Buffer.from(JSON.stringify(req.body));
|
||||
rawBody = Buffer.from(JSON.stringify(req.body))
|
||||
}
|
||||
|
||||
if (!rawBody) {
|
||||
res.status(400).send("Missing request body");
|
||||
return;
|
||||
res.status(400).send('Missing request body')
|
||||
return
|
||||
}
|
||||
|
||||
let stripe = null;
|
||||
let stripe = null
|
||||
try {
|
||||
stripe = getStripeClient();
|
||||
stripe = getStripeClient()
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[subscription-handleStripeWebhook] Stripe client error",
|
||||
error,
|
||||
);
|
||||
res.status(500).send("Client Stripe indisponible");
|
||||
return;
|
||||
console.error('[subscription-handleStripeWebhook] Stripe client error', error)
|
||||
res.status(500).send('Client Stripe indisponible')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
rawBody,
|
||||
signature,
|
||||
resolveStripeWebhookSecret(),
|
||||
);
|
||||
const event = stripe.webhooks.constructEvent(rawBody, signature, resolveStripeWebhookSecret())
|
||||
|
||||
await handleStripeWebhookEvent({ event, stripe });
|
||||
await handleStripeWebhookEvent({ event, stripe })
|
||||
|
||||
res.status(200).send({ received: true });
|
||||
res.status(200).send({ received: true })
|
||||
} catch (error) {
|
||||
console.error("[subscription-handleStripeWebhook] error", error);
|
||||
console.error('[subscription-handleStripeWebhook] error', error)
|
||||
if (error instanceof HttpsError) {
|
||||
res.status(400).send(error.message);
|
||||
return;
|
||||
res.status(400).send(error.message)
|
||||
return
|
||||
}
|
||||
res.status(500).send("Erreur lors du traitement du webhook");
|
||||
res.status(500).send('Erreur lors du traitement du webhook')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
handleStripeWebhook,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user