fix: phone number billing bug (#23851)
* fix: phone number billing bug * fix: phone number billing bug * fix: use raw code * fix: use zod * chore: update test * Remove newline. --------- Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
Alex van Andel
parent
3aca339c95
commit
0c2ba81269
@@ -1,3 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { getStripeCustomerIdFromUserId } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import { getPhoneNumberMonthlyPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { CHECKOUT_SESSION_TYPES } from "@calcom/features/ee/billing/constants";
|
||||
@@ -10,6 +12,15 @@ import { PhoneNumberSubscriptionStatus } from "@calcom/prisma/enums";
|
||||
import type { PhoneNumberRepositoryInterface } from "../../interfaces/PhoneNumberRepositoryInterface";
|
||||
import type { RetellAIRepository } from "../types";
|
||||
|
||||
const stripeResourceMissingErrorSchema = z.object({
|
||||
type: z.literal("invalid_request_error"),
|
||||
code: z.literal("resource_missing"),
|
||||
message: z.string(),
|
||||
param: z.string().optional(),
|
||||
doc_url: z.string().optional(),
|
||||
request_log_url: z.string().optional(),
|
||||
});
|
||||
|
||||
export class BillingService {
|
||||
private logger = logger.getSubLogger({ prefix: ["BillingService"] });
|
||||
|
||||
@@ -129,7 +140,20 @@ export class BillingService {
|
||||
}
|
||||
|
||||
try {
|
||||
await stripe.subscriptions.cancel(phoneNumber.stripeSubscriptionId);
|
||||
try {
|
||||
await stripe.subscriptions.cancel(phoneNumber.stripeSubscriptionId);
|
||||
} catch (error) {
|
||||
const parsedError = stripeResourceMissingErrorSchema.safeParse(error);
|
||||
if (parsedError.success) {
|
||||
this.logger.info("Subscription not found in Stripe (already cancelled or deleted):", {
|
||||
subscriptionId: phoneNumber.stripeSubscriptionId,
|
||||
phoneNumberId,
|
||||
stripeMessage: parsedError.data.message,
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await this.phoneNumberRepository.updateSubscriptionStatus({
|
||||
id: phoneNumberId,
|
||||
|
||||
+59
@@ -254,5 +254,64 @@ describe("BillingService", () => {
|
||||
"Failed to cancel subscription"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle subscription not found (404) gracefully", async () => {
|
||||
const mockPhoneNumber = createMockPhoneNumberRecord({
|
||||
id: 1,
|
||||
stripeSubscriptionId: "sub_123",
|
||||
subscriptionStatus: PhoneNumberSubscriptionStatus.ACTIVE,
|
||||
});
|
||||
|
||||
mocks.mockPhoneNumberRepository.findByIdAndUserId.mockResolvedValue(mockPhoneNumber);
|
||||
mocks.mockPhoneNumberRepository.updateSubscriptionStatus.mockResolvedValue(undefined);
|
||||
mocks.mockRetellRepository.deletePhoneNumber.mockResolvedValue(undefined);
|
||||
|
||||
const stripe = (await import("@calcom/features/ee/payments/server/stripe")).default;
|
||||
stripe.subscriptions.cancel.mockRejectedValue({
|
||||
type: "invalid_request_error",
|
||||
code: "resource_missing",
|
||||
message: "No such subscription: 'sub_123'",
|
||||
param: "id",
|
||||
doc_url: "https://stripe.com/docs/error-codes/resource-missing",
|
||||
});
|
||||
|
||||
const result = await service.cancelPhoneNumberSubscription(validCancelData);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
message: "Phone number subscription cancelled successfully.",
|
||||
});
|
||||
|
||||
// Should attempt to cancel
|
||||
expect(stripe.subscriptions.cancel).toHaveBeenCalledWith("sub_123");
|
||||
// Should still update database even after 404
|
||||
expect(mocks.mockPhoneNumberRepository.updateSubscriptionStatus).toHaveBeenCalledWith({
|
||||
id: 1,
|
||||
subscriptionStatus: PhoneNumberSubscriptionStatus.CANCELLED,
|
||||
disconnectOutboundAgent: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw error on Stripe API failure that is not resource_missing", async () => {
|
||||
const mockPhoneNumber = createMockPhoneNumberRecord({
|
||||
id: 1,
|
||||
stripeSubscriptionId: "sub_123",
|
||||
subscriptionStatus: PhoneNumberSubscriptionStatus.ACTIVE,
|
||||
});
|
||||
|
||||
mocks.mockPhoneNumberRepository.findByIdAndUserId.mockResolvedValue(mockPhoneNumber);
|
||||
|
||||
const stripe = (await import("@calcom/features/ee/payments/server/stripe")).default;
|
||||
stripe.subscriptions.cancel.mockRejectedValue(new TestError("API Error"));
|
||||
|
||||
await expect(service.cancelPhoneNumberSubscription(validCancelData)).rejects.toThrow(
|
||||
"Failed to cancel subscription"
|
||||
);
|
||||
|
||||
// Should attempt to cancel
|
||||
expect(stripe.subscriptions.cancel).toHaveBeenCalledWith("sub_123");
|
||||
// Should NOT update database due to error
|
||||
expect(mocks.mockPhoneNumberRepository.updateSubscriptionStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user