mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
refactor: improve logging and code formatting in billing and project services
This commit is contained in:
@@ -2,14 +2,13 @@ import { BillingWebhookSecret, IsBillingEnabled } from "../EnvironmentConfig";
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import BillingService from "../Services/BillingService";
|
||||
import ProjectService from "../Services/ProjectService";
|
||||
import {
|
||||
import Express, {
|
||||
ExpressRequest,
|
||||
ExpressResponse,
|
||||
ExpressRouter,
|
||||
NextFunction,
|
||||
OneUptimeRequest,
|
||||
} from "../Utils/Express";
|
||||
import Express from "../Utils/Express";
|
||||
import Response from "../Utils/Response";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Permission, { UserPermission } from "../../Types/Permission";
|
||||
@@ -30,41 +29,60 @@ export default class BillingAPI {
|
||||
`/billing/webhook`,
|
||||
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
||||
try {
|
||||
logger.debug(`[Invoice Email] Webhook endpoint hit - /billing/webhook`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Webhook endpoint hit - /billing/webhook`,
|
||||
);
|
||||
|
||||
if (!IsBillingEnabled) {
|
||||
logger.debug(`[Invoice Email] Billing not enabled, returning early`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Billing not enabled, returning early`,
|
||||
);
|
||||
return Response.sendJsonObjectResponse(req, res, {
|
||||
message: "Billing is not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
if (!BillingWebhookSecret) {
|
||||
logger.error(`[Invoice Email] Billing webhook secret is not configured`);
|
||||
logger.error(
|
||||
`[Invoice Email] Billing webhook secret is not configured`,
|
||||
);
|
||||
throw new BadDataException(
|
||||
"Billing webhook secret is not configured",
|
||||
);
|
||||
}
|
||||
|
||||
const signature = req.headers["stripe-signature"] as string;
|
||||
logger.debug(`[Invoice Email] Stripe signature header present: ${!!signature}`);
|
||||
const signature: string = req.headers["stripe-signature"] as string;
|
||||
logger.debug(
|
||||
`[Invoice Email] Stripe signature header present: ${Boolean(signature)}`,
|
||||
);
|
||||
|
||||
if (!signature) {
|
||||
logger.error(`[Invoice Email] Missing Stripe signature header`);
|
||||
throw new BadDataException("Missing Stripe signature header");
|
||||
}
|
||||
|
||||
const rawBody = (req as OneUptimeRequest).rawBody;
|
||||
logger.debug(`[Invoice Email] Raw body present: ${!!rawBody}, length: ${rawBody?.length || 0}`);
|
||||
const rawBody: string | undefined = (req as OneUptimeRequest).rawBody;
|
||||
logger.debug(
|
||||
`[Invoice Email] Raw body present: ${Boolean(rawBody)}, length: ${rawBody?.length || 0}`,
|
||||
);
|
||||
|
||||
if (!rawBody) {
|
||||
logger.error(`[Invoice Email] Missing raw body for webhook verification`);
|
||||
throw new BadDataException("Missing raw body for webhook verification");
|
||||
logger.error(
|
||||
`[Invoice Email] Missing raw body for webhook verification`,
|
||||
);
|
||||
throw new BadDataException(
|
||||
"Missing raw body for webhook verification",
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`[Invoice Email] Verifying webhook signature...`);
|
||||
const event = BillingService.verifyWebhookSignature(rawBody, signature);
|
||||
logger.debug(`[Invoice Email] Webhook signature verified successfully, event type: ${event.type}`);
|
||||
const event: Stripe.Event = BillingService.verifyWebhookSignature(
|
||||
rawBody,
|
||||
signature,
|
||||
);
|
||||
logger.debug(
|
||||
`[Invoice Email] Webhook signature verified successfully, event type: ${event.type}`,
|
||||
);
|
||||
|
||||
// Handle the event asynchronously
|
||||
logger.debug(`[Invoice Email] Handling webhook event...`);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const IsBillingEnabled: boolean = process.env["BILLING_ENABLED"] === "true";
|
||||
const BillingPublicKey: string = process.env["BILLING_PUBLIC_KEY"] || "";
|
||||
const BillingPrivateKey: string = process.env["BILLING_PRIVATE_KEY"] || "";
|
||||
const BillingWebhookSecret: string = process.env["BILLING_WEBHOOK_SECRET"] || "";
|
||||
const BillingWebhookSecret: string =
|
||||
process.env["BILLING_WEBHOOK_SECRET"] || "";
|
||||
|
||||
export default {
|
||||
IsBillingEnabled,
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1769599843642 implements MigrationInterface {
|
||||
public name = 'MigrationName1769599843642'
|
||||
public name = "MigrationName1769599843642";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Project" ADD "sendInvoicesByEmail" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "Project" DROP COLUMN "sendInvoicesByEmail"`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" ADD "sendInvoicesByEmail" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "Project" DROP COLUMN "sendInvoicesByEmail"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,5 +477,5 @@ export default [
|
||||
MigrationName1769428821686,
|
||||
MigrationName1769469813786,
|
||||
RenameNotificationRuleTypes1769517677937,
|
||||
MigrationName1769599843642
|
||||
MigrationName1769599843642,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { BillingPrivateKey, BillingWebhookSecret, IsBillingEnabled, DashboardClientUrl } from "../EnvironmentConfig";
|
||||
import {
|
||||
BillingPrivateKey,
|
||||
BillingWebhookSecret,
|
||||
IsBillingEnabled,
|
||||
DashboardClientUrl,
|
||||
} from "../EnvironmentConfig";
|
||||
import Project from "../../Models/DatabaseModels/Project";
|
||||
import ServerMeteredPlan from "../Types/Billing/MeteredPlan/ServerMeteredPlan";
|
||||
import Errors from "../Utils/Errors";
|
||||
@@ -91,10 +96,14 @@ export class BillingService extends BaseService {
|
||||
financeAccountingEmail?: string | null,
|
||||
sendInvoicesByEmail?: boolean | null,
|
||||
): Promise<void> {
|
||||
logger.debug(`[Invoice Email] updateCustomerBusinessDetails called - customerId: ${id}, sendInvoicesByEmail: ${sendInvoicesByEmail}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] updateCustomerBusinessDetails called - customerId: ${id}, sendInvoicesByEmail: ${sendInvoicesByEmail}`,
|
||||
);
|
||||
|
||||
if (!this.isBillingEnabled()) {
|
||||
logger.debug(`[Invoice Email] Billing not enabled, skipping updateCustomerBusinessDetails for customer ${id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Billing not enabled, skipping updateCustomerBusinessDetails for customer ${id}`,
|
||||
);
|
||||
throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
|
||||
}
|
||||
/*
|
||||
@@ -141,8 +150,12 @@ export class BillingService extends BaseService {
|
||||
metadata["finance_accounting_email"] = "";
|
||||
}
|
||||
if (sendInvoicesByEmail !== undefined && sendInvoicesByEmail !== null) {
|
||||
metadata["send_invoices_by_email"] = sendInvoicesByEmail ? "true" : "false";
|
||||
logger.debug(`[Invoice Email] Setting send_invoices_by_email metadata to "${metadata["send_invoices_by_email"]}" for customer ${id}`);
|
||||
metadata["send_invoices_by_email"] = sendInvoicesByEmail
|
||||
? "true"
|
||||
: "false";
|
||||
logger.debug(
|
||||
`[Invoice Email] Setting send_invoices_by_email metadata to "${metadata["send_invoices_by_email"]}" for customer ${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updateParams: Stripe.CustomerUpdateParams = {
|
||||
@@ -180,7 +193,9 @@ export class BillingService extends BaseService {
|
||||
} as any;
|
||||
}
|
||||
|
||||
logger.debug(`[Invoice Email] Updating Stripe customer ${id} with metadata: ${JSON.stringify(metadata)}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Updating Stripe customer ${id} with metadata: ${JSON.stringify(metadata)}`,
|
||||
);
|
||||
await this.stripe.customers.update(id, updateParams);
|
||||
logger.debug(`[Invoice Email] Successfully updated Stripe customer ${id}`);
|
||||
}
|
||||
@@ -943,20 +958,28 @@ export class BillingService extends BaseService {
|
||||
recipientEmail?: Email,
|
||||
projectId?: ObjectID,
|
||||
): Promise<void> {
|
||||
logger.debug(`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId}, recipientEmail: ${recipientEmail?.toString()}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] sendInvoiceByEmail called for invoice: ${invoiceId}, recipientEmail: ${recipientEmail?.toString()}`,
|
||||
);
|
||||
|
||||
if (!this.isBillingEnabled()) {
|
||||
logger.debug(`[Invoice Email] Billing not enabled, skipping send for invoice: ${invoiceId}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Billing not enabled, skipping send for invoice: ${invoiceId}`,
|
||||
);
|
||||
throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch invoice details from Stripe
|
||||
logger.debug(`[Invoice Email] Fetching invoice ${invoiceId} details from Stripe`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Fetching invoice ${invoiceId} details from Stripe`,
|
||||
);
|
||||
const stripeInvoice = await this.stripe.invoices.retrieve(invoiceId);
|
||||
|
||||
if (!stripeInvoice) {
|
||||
logger.error(`[Invoice Email] Invoice ${invoiceId} not found in Stripe`);
|
||||
logger.error(
|
||||
`[Invoice Email] Invoice ${invoiceId} not found in Stripe`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -967,16 +990,22 @@ export class BillingService extends BaseService {
|
||||
}
|
||||
|
||||
if (!toEmail) {
|
||||
logger.error(`[Invoice Email] No recipient email found for invoice ${invoiceId}`);
|
||||
logger.error(
|
||||
`[Invoice Email] No recipient email found for invoice ${invoiceId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Format invoice data for email
|
||||
const invoiceNumber = stripeInvoice.number || invoiceId;
|
||||
const invoiceDate = stripeInvoice.created
|
||||
? OneUptimeDate.getDateAsFormattedString(new Date(stripeInvoice.created * 1000))
|
||||
: OneUptimeDate.getDateAsFormattedString(OneUptimeDate.getCurrentDate());
|
||||
const amount = `${(stripeInvoice.amount_due / 100).toFixed(2)} ${stripeInvoice.currency?.toUpperCase() || 'USD'}`;
|
||||
? OneUptimeDate.getDateAsFormattedString(
|
||||
new Date(stripeInvoice.created * 1000),
|
||||
)
|
||||
: OneUptimeDate.getDateAsFormattedString(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
);
|
||||
const amount = `${(stripeInvoice.amount_due / 100).toFixed(2)} ${stripeInvoice.currency?.toUpperCase() || "USD"}`;
|
||||
const invoicePdfUrl = stripeInvoice.invoice_pdf || undefined;
|
||||
const description = stripeInvoice.description || undefined;
|
||||
|
||||
@@ -986,7 +1015,9 @@ export class BillingService extends BaseService {
|
||||
dashboardLink = `${DashboardClientUrl.toString()}/dashboard/${projectId.toString()}/settings/billing`;
|
||||
}
|
||||
|
||||
logger.debug(`[Invoice Email] Sending invoice email to ${toEmail.toString()} - Invoice #${invoiceNumber}, Amount: ${amount}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Sending invoice email to ${toEmail.toString()} - Invoice #${invoiceNumber}, Amount: ${amount}`,
|
||||
);
|
||||
|
||||
// Send email via OneUptime MailService
|
||||
await MailService.sendMail(
|
||||
@@ -997,9 +1028,9 @@ export class BillingService extends BaseService {
|
||||
invoiceNumber: invoiceNumber,
|
||||
invoiceDate: invoiceDate,
|
||||
amount: amount,
|
||||
description: description || '',
|
||||
invoicePdfUrl: invoicePdfUrl || '',
|
||||
dashboardLink: dashboardLink || '',
|
||||
description: description || "",
|
||||
invoicePdfUrl: invoicePdfUrl || "",
|
||||
dashboardLink: dashboardLink || "",
|
||||
},
|
||||
subject: `Invoice #${invoiceNumber} from OneUptime`,
|
||||
},
|
||||
@@ -1008,38 +1039,56 @@ export class BillingService extends BaseService {
|
||||
},
|
||||
);
|
||||
|
||||
logger.debug(`[Invoice Email] Successfully sent invoice ${invoiceId} email to ${toEmail.toString()}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Successfully sent invoice ${invoiceId} email to ${toEmail.toString()}`,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`[Invoice Email] Failed to send invoice ${invoiceId} by email: ${err}`);
|
||||
logger.error(
|
||||
`[Invoice Email] Failed to send invoice ${invoiceId} by email: ${err}`,
|
||||
);
|
||||
// Don't throw - sending email is not critical
|
||||
}
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async shouldSendInvoicesByEmail(customerId: string): Promise<boolean> {
|
||||
logger.debug(`[Invoice Email] shouldSendInvoicesByEmail called for customer: ${customerId}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] shouldSendInvoicesByEmail called for customer: ${customerId}`,
|
||||
);
|
||||
|
||||
if (!this.isBillingEnabled()) {
|
||||
logger.debug(`[Invoice Email] Billing not enabled, returning false for customer: ${customerId}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Billing not enabled, returning false for customer: ${customerId}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(`[Invoice Email] Retrieving customer ${customerId} from Stripe to check preference`);
|
||||
const customer: Stripe.Response<Stripe.Customer | Stripe.DeletedCustomer> =
|
||||
await this.stripe.customers.retrieve(customerId);
|
||||
logger.debug(
|
||||
`[Invoice Email] Retrieving customer ${customerId} from Stripe to check preference`,
|
||||
);
|
||||
const customer: Stripe.Response<
|
||||
Stripe.Customer | Stripe.DeletedCustomer
|
||||
> = await this.stripe.customers.retrieve(customerId);
|
||||
|
||||
if (!customer || customer.deleted) {
|
||||
logger.debug(`[Invoice Email] Customer ${customerId} not found or deleted, returning false`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Customer ${customerId} not found or deleted, returning false`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const metadata = (customer as Stripe.Customer).metadata;
|
||||
const sendInvoicesByEmail = metadata?.["send_invoices_by_email"] === "true";
|
||||
logger.debug(`[Invoice Email] Customer ${customerId} metadata.send_invoices_by_email = "${metadata?.["send_invoices_by_email"]}", result: ${sendInvoicesByEmail}`);
|
||||
const sendInvoicesByEmail =
|
||||
metadata?.["send_invoices_by_email"] === "true";
|
||||
logger.debug(
|
||||
`[Invoice Email] Customer ${customerId} metadata.send_invoices_by_email = "${metadata?.["send_invoices_by_email"]}", result: ${sendInvoicesByEmail}`,
|
||||
);
|
||||
return sendInvoicesByEmail;
|
||||
} catch (err) {
|
||||
logger.error(`[Invoice Email] Failed to check invoice email preference for customer ${customerId}: ${err}`);
|
||||
logger.error(
|
||||
`[Invoice Email] Failed to check invoice email preference for customer ${customerId}: ${err}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1059,7 +1108,9 @@ export class BillingService extends BaseService {
|
||||
const recipientEmail = options?.recipientEmail;
|
||||
const projectId = options?.projectId;
|
||||
|
||||
logger.debug(`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail}, recipientEmail: ${recipientEmail?.toString()}, projectId: ${projectId?.toString()}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] generateInvoiceAndChargeCustomer called - customer: ${customerId}, amount: $${amountInUsd}, sendInvoiceByEmail: ${sendInvoiceByEmail}, recipientEmail: ${recipientEmail?.toString()}, projectId: ${projectId?.toString()}`,
|
||||
);
|
||||
|
||||
const invoice: Stripe.Invoice = await this.stripe.invoices.create({
|
||||
customer: customerId,
|
||||
@@ -1068,11 +1119,15 @@ export class BillingService extends BaseService {
|
||||
});
|
||||
|
||||
if (!invoice || !invoice.id) {
|
||||
logger.error(`[Invoice Email] Failed to create invoice for customer ${customerId}`);
|
||||
logger.error(
|
||||
`[Invoice Email] Failed to create invoice for customer ${customerId}`,
|
||||
);
|
||||
throw new APIException(Errors.BillingService.INVOICE_NOT_GENERATED);
|
||||
}
|
||||
|
||||
logger.debug(`[Invoice Email] Created invoice ${invoice.id} for customer ${customerId}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Created invoice ${invoice.id} for customer ${customerId}`,
|
||||
);
|
||||
|
||||
await this.stripe.invoiceItems.create({
|
||||
invoice: invoice.id,
|
||||
@@ -1081,7 +1136,9 @@ export class BillingService extends BaseService {
|
||||
customer: customerId,
|
||||
});
|
||||
|
||||
logger.debug(`[Invoice Email] Added invoice item to invoice ${invoice.id}: ${itemText}, $${amountInUsd}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Added invoice item to invoice ${invoice.id}: ${itemText}, $${amountInUsd}`,
|
||||
);
|
||||
|
||||
await this.stripe.invoices.finalizeInvoice(invoice.id!);
|
||||
logger.debug(`[Invoice Email] Finalized invoice ${invoice.id}`);
|
||||
@@ -1092,13 +1149,19 @@ export class BillingService extends BaseService {
|
||||
|
||||
// Send invoice by email if requested
|
||||
if (sendInvoiceByEmail) {
|
||||
logger.debug(`[Invoice Email] sendInvoiceByEmail is true, sending invoice ${invoice.id} by email`);
|
||||
logger.debug(
|
||||
`[Invoice Email] sendInvoiceByEmail is true, sending invoice ${invoice.id} by email`,
|
||||
);
|
||||
await this.sendInvoiceByEmail(invoice.id!, recipientEmail, projectId);
|
||||
} else {
|
||||
logger.debug(`[Invoice Email] sendInvoiceByEmail is false, skipping email for invoice ${invoice.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] sendInvoiceByEmail is false, skipping email for invoice ${invoice.id}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[Invoice Email] Failed to pay invoice ${invoice.id}, voiding: ${err}`);
|
||||
logger.error(
|
||||
`[Invoice Email] Failed to pay invoice ${invoice.id}, voiding: ${err}`,
|
||||
);
|
||||
// mark invoice as failed and do not collect payment.
|
||||
await this.voidInvoice(invoice.id!);
|
||||
throw err;
|
||||
@@ -1195,49 +1258,70 @@ export class BillingService extends BaseService {
|
||||
throw new BadDataException("Billing webhook secret is not configured");
|
||||
}
|
||||
|
||||
logger.debug(`[Invoice Email] Verifying webhook signature with secret (length: ${BillingWebhookSecret.length})`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Verifying webhook signature with secret (length: ${BillingWebhookSecret.length})`,
|
||||
);
|
||||
const event = this.stripe.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
BillingWebhookSecret,
|
||||
);
|
||||
logger.debug(`[Invoice Email] Webhook signature verified, event type: ${event.type}, event id: ${event.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Webhook signature verified, event type: ${event.type}, event id: ${event.id}`,
|
||||
);
|
||||
return event;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async handleWebhookEvent(event: Stripe.Event): Promise<void> {
|
||||
logger.debug(`[Invoice Email] handleWebhookEvent called - event type: ${event.type}, event id: ${event.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] handleWebhookEvent called - event type: ${event.type}, event id: ${event.id}`,
|
||||
);
|
||||
|
||||
if (!this.isBillingEnabled()) {
|
||||
logger.debug(`[Invoice Email] Billing not enabled, ignoring webhook event ${event.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Billing not enabled, ignoring webhook event ${event.id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle invoice.finalized event to send invoice by email if customer has opted in
|
||||
if (event.type === "invoice.finalized") {
|
||||
logger.debug(`[Invoice Email] Processing invoice.finalized event ${event.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Processing invoice.finalized event ${event.id}`,
|
||||
);
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
logger.debug(`[Invoice Email] Invoice details - id: ${invoice.id}, number: ${invoice.number}, customer: ${invoice.customer}, status: ${invoice.status}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Invoice details - id: ${invoice.id}, number: ${invoice.number}, customer: ${invoice.customer}, status: ${invoice.status}`,
|
||||
);
|
||||
|
||||
if (!invoice.customer) {
|
||||
logger.debug(`[Invoice Email] No customer on invoice ${invoice.id}, skipping`);
|
||||
logger.debug(
|
||||
`[Invoice Email] No customer on invoice ${invoice.id}, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const customerId = typeof invoice.customer === "string"
|
||||
? invoice.customer
|
||||
: invoice.customer.id;
|
||||
const customerId =
|
||||
typeof invoice.customer === "string"
|
||||
? invoice.customer
|
||||
: invoice.customer.id;
|
||||
|
||||
logger.debug(`[Invoice Email] Extracted customer ID: ${customerId} from invoice ${invoice.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Extracted customer ID: ${customerId} from invoice ${invoice.id}`,
|
||||
);
|
||||
|
||||
try {
|
||||
logger.debug(`[Invoice Email] Checking if customer ${customerId} has invoice emails enabled`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Checking if customer ${customerId} has invoice emails enabled`,
|
||||
);
|
||||
const shouldSend = await this.shouldSendInvoicesByEmail(customerId);
|
||||
|
||||
if (shouldSend && invoice.id) {
|
||||
logger.debug(`[Invoice Email] Customer ${customerId} has invoice emails enabled, looking up project`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Customer ${customerId} has invoice emails enabled, looking up project`,
|
||||
);
|
||||
|
||||
// Lazy import to avoid circular dependency
|
||||
const { default: ProjectService } = await import("./ProjectService");
|
||||
@@ -1264,23 +1348,37 @@ export class BillingService extends BaseService {
|
||||
if (project.financeAccountingEmail) {
|
||||
recipientEmail = new Email(project.financeAccountingEmail);
|
||||
}
|
||||
logger.debug(`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail: ${recipientEmail?.toString()}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Found project ${projectId?.toString()}, financeAccountingEmail: ${recipientEmail?.toString()}`,
|
||||
);
|
||||
} else {
|
||||
logger.debug(`[Invoice Email] No project found for customer ${customerId}, will use Stripe customer email`);
|
||||
logger.debug(
|
||||
`[Invoice Email] No project found for customer ${customerId}, will use Stripe customer email`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`[Invoice Email] Sending invoice ${invoice.id} by email`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Sending invoice ${invoice.id} by email`,
|
||||
);
|
||||
await this.sendInvoiceByEmail(invoice.id, recipientEmail, projectId);
|
||||
logger.debug(`[Invoice Email] Successfully processed invoice.finalized - sent invoice ${invoice.id} by email`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Successfully processed invoice.finalized - sent invoice ${invoice.id} by email`,
|
||||
);
|
||||
} else {
|
||||
logger.debug(`[Invoice Email] Customer ${customerId} has invoice emails disabled (shouldSend: ${shouldSend}), skipping email for invoice ${invoice.id}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Customer ${customerId} has invoice emails disabled (shouldSend: ${shouldSend}), skipping email for invoice ${invoice.id}`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[Invoice Email] Failed to send invoice by email for invoice ${invoice.id}: ${err}`);
|
||||
logger.error(
|
||||
`[Invoice Email] Failed to send invoice by email for invoice ${invoice.id}: ${err}`,
|
||||
);
|
||||
// Don't throw - webhook should still return success
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[Invoice Email] Ignoring event type ${event.type}, not invoice.finalized`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Ignoring event type ${event.type}, not invoice.finalized`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,8 +283,12 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
updateBy.data.financeAccountingEmail ||
|
||||
updateBy.data.sendInvoicesByEmail !== undefined
|
||||
) {
|
||||
logger.debug(`[Invoice Email] ProjectService.onBeforeUpdate - syncing billing details to Stripe`);
|
||||
logger.debug(`[Invoice Email] Fields being updated - businessDetails: ${!!updateBy.data.businessDetails}, businessDetailsCountry: ${!!updateBy.data.businessDetailsCountry}, financeAccountingEmail: ${!!updateBy.data.financeAccountingEmail}, sendInvoicesByEmail: ${updateBy.data.sendInvoicesByEmail}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] ProjectService.onBeforeUpdate - syncing billing details to Stripe`,
|
||||
);
|
||||
logger.debug(
|
||||
`[Invoice Email] Fields being updated - businessDetails: ${Boolean(updateBy.data.businessDetails)}, businessDetailsCountry: ${Boolean(updateBy.data.businessDetailsCountry)}, financeAccountingEmail: ${Boolean(updateBy.data.financeAccountingEmail)}, sendInvoicesByEmail: ${updateBy.data.sendInvoicesByEmail}`,
|
||||
);
|
||||
|
||||
// Sync to Stripe.
|
||||
const project: Model | null = await this.findOneById({
|
||||
@@ -297,15 +301,20 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
props: { isRoot: true },
|
||||
});
|
||||
|
||||
logger.debug(`[Invoice Email] Project found - paymentProviderCustomerId: ${project?.paymentProviderCustomerId}, existing sendInvoicesByEmail: ${(project as any)?.sendInvoicesByEmail}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Project found - paymentProviderCustomerId: ${project?.paymentProviderCustomerId}, existing sendInvoicesByEmail: ${(project as any)?.sendInvoicesByEmail}`,
|
||||
);
|
||||
|
||||
if (project?.paymentProviderCustomerId) {
|
||||
try {
|
||||
const sendInvoicesByEmailValue = updateBy.data.sendInvoicesByEmail !== undefined
|
||||
? (updateBy.data.sendInvoicesByEmail as boolean)
|
||||
: (project as any).sendInvoicesByEmail || null;
|
||||
const sendInvoicesByEmailValue =
|
||||
updateBy.data.sendInvoicesByEmail !== undefined
|
||||
? (updateBy.data.sendInvoicesByEmail as boolean)
|
||||
: (project as any).sendInvoicesByEmail || null;
|
||||
|
||||
logger.debug(`[Invoice Email] Calling BillingService.updateCustomerBusinessDetails with sendInvoicesByEmail: ${sendInvoicesByEmailValue}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Calling BillingService.updateCustomerBusinessDetails with sendInvoicesByEmail: ${sendInvoicesByEmailValue}`,
|
||||
);
|
||||
|
||||
await BillingService.updateCustomerBusinessDetails(
|
||||
project.paymentProviderCustomerId,
|
||||
@@ -317,14 +326,18 @@ export class ProjectService extends DatabaseService<Model> {
|
||||
sendInvoicesByEmailValue,
|
||||
);
|
||||
|
||||
logger.debug(`[Invoice Email] Successfully synced billing details to Stripe for customer ${project.paymentProviderCustomerId}`);
|
||||
logger.debug(
|
||||
`[Invoice Email] Successfully synced billing details to Stripe for customer ${project.paymentProviderCustomerId}`,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[Invoice Email] Failed to update Stripe customer business details: ${err}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`[Invoice Email] No paymentProviderCustomerId found, skipping Stripe sync`);
|
||||
logger.debug(
|
||||
`[Invoice Email] No paymentProviderCustomerId found, skipping Stripe sync`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (updateBy.data.enableAutoRechargeSmsOrCallBalance) {
|
||||
|
||||
@@ -22,14 +22,17 @@ import Project from "Common/Models/DatabaseModels/Project";
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import useAsyncEffect from "use-async-effect";
|
||||
|
||||
const DeleteAccount: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const DeleteAccount: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [projects, setProjects] = useState<Array<Project>>([]);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
|
||||
const [isDeleting, setIsDeleting] = useState<boolean>(false);
|
||||
const [deleteError, setDeleteError] = useState<string>("");
|
||||
const [showDeleteErrorModal, setShowDeleteErrorModal] = useState<boolean>(false);
|
||||
const [showDeleteErrorModal, setShowDeleteErrorModal] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const userId: ObjectID = UserUtil.getUserId();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user