diff --git a/packages/backend/src/server/StripeWebhookServerService.ts b/packages/backend/src/server/StripeWebhookServerService.ts index 099c48af8c9c..2518d97de973 100644 --- a/packages/backend/src/server/StripeWebhookServerService.ts +++ b/packages/backend/src/server/StripeWebhookServerService.ts @@ -31,7 +31,7 @@ export class StripeWebhookServerService { private globalEventService: GlobalEventService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('server', 'gray', false); + this.logger = this.loggerService.getLogger('subscription:webhook'); } @bindThis @@ -50,22 +50,27 @@ export class StripeWebhookServerService { ); fastify.post('/webhook', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => { + /* サブスクリプションの機能が無効にされていてもWebhookは処理するようにする。 const instance = await this.metaService.fetch(true); if (!(instance.enableSubscriptions)) { return reply.code(503); } + */ if (!(this.config.stripe && this.config.stripe.secretKey && this.config.stripe.webhookSecret)) { + this.logger.error('The Stripe webhook configuration is not set correctly.'); return reply.code(503); } const body = request.rawBody; if (!body) { + this.logger.error('Request body from Stripe webhook is empty.'); return reply.code(400); } // Retrieve the event by verifying the signature using the raw body and secret. const signature = request.headers['stripe-signature']; if (!signature) { // Check if signature exists. + this.logger.error('Webhook does not contain a signature.'); return reply.code(400); } @@ -74,6 +79,7 @@ export class StripeWebhookServerService { try { event = stripe.webhooks.constructEvent(body, signature, this.config.stripe.webhookSecret); } catch (err) { + this.logger.error('Webhook signature verification or event parsing failed.', { error: err }); return reply.code(400); } @@ -83,6 +89,7 @@ export class StripeWebhookServerService { const userProfile = await this.userProfilesRepository.findOneBy({ stripeCustomerId: customer }); if (!userProfile) { + this.logger.warn(`CustomerId: "${customer}" has no user profile found.`); return reply.code(400); } reply.code(204); // Stripeへの応答を設定 @@ -98,6 +105,7 @@ export class StripeWebhookServerService { const user = await this.usersRepository.findOneByOrFail({ id: userProfile.userId }); if (user.stripeSubscriptionId != null) { + this.logger.info(`Subscription already exists for user ID ${user.id}. No processing is needed.`); return; // 既にサブスクリプションが存在する場合は何もしない } @@ -106,6 +114,7 @@ export class StripeWebhookServerService { // ユーザーにロールが割り当てられていない場合、ロールを割り当てる if (!roles.some((role) => role.id === subscriptionPlan.roleId)) { await this.roleService.assign(userProfile.userId, subscriptionPlan.roleId); + this.logger.info(`${userProfile.userId} has been assigned the role "${subscriptionPlan.roleId}" by the subscription creation event.`); } }); } @@ -141,12 +150,14 @@ export class StripeWebhookServerService { for (const role of roles) { if (roleIds.includes(role.id) && role.id !== subscriptionPlan.roleId) { await this.roleService.unassign(user.id, role.id); // 他のサブスクリプションプランのロールが割り当てられている場合、ロールを解除する + this.logger.info(`${user.id} has been unassigned the role "${role.id}" by the subscription update event.`); } } // ユーザーにロールが割り当てられていない場合、ロールを割り当てる if (!roles.some((role) => role.id === subscriptionPlan.roleId)) { await this.roleService.assign(user.id, subscriptionPlan.roleId); + this.logger.info(`${user.id} has been assigned the role "${subscriptionPlan.roleId}" by the subscription update event.`); } }); } else if (subscriptionPlan.id !== user.subscriptionPlanId) { // サブスクリプションプランが変更された場合 @@ -155,11 +166,13 @@ export class StripeWebhookServerService { // 旧サブスクリプションプランのロールが割り当てられている場合、ロールを解除する if (roles.some((role) => role.id === oldSubscriptionPlan.roleId)) { await this.roleService.unassign(user.id, oldSubscriptionPlan.roleId); + this.logger.info(`${user.id} has been unassigned the role "${oldSubscriptionPlan.roleId}" by the subscription update event.`); } - + // 新しいサブスクリプションプランのロールが割り当てられていない場合、ロールを割り当てる if (!roles.some((role) => role.id === subscriptionPlan.roleId)) { await this.roleService.assign(user.id, subscriptionPlan.roleId); + this.logger.info(`${user.id} has been assigned the role "${subscriptionPlan.roleId}" by the subscription update event.`); } }); } else if (previousData && previousData.status) { // サブスクリプションステータスが変更された場合 @@ -167,6 +180,7 @@ export class StripeWebhookServerService { // ユーザーにロールが割り当てられていない場合、ロールを割り当てる if (!roles.some((role) => role.id === subscriptionPlan.roleId)) { await this.roleService.assign(user.id, subscriptionPlan.roleId); + this.logger.info(`${user.id} has been assigned the role "${subscriptionPlan.roleId}" by the subscription update event.`); } }); } @@ -202,6 +216,7 @@ export class StripeWebhookServerService { await this.roleService.getUserRoles(userProfile.userId).then(async (roles) => { if (roles.some((role) => role.id === subscriptionPlan.roleId)) { await this.roleService.unassign(userProfile.userId, subscriptionPlan.roleId); + this.logger.info(`${userProfile.userId} has been unassigned the role "${subscriptionPlan.roleId}" by the subscription deletion event.`); } }); @@ -222,6 +237,7 @@ export class StripeWebhookServerService { default: // Unhandled event type. + this.logger.warn(`Unhandled event type: ${event.type}`); return reply.code(400); } }); diff --git a/packages/backend/src/server/api/endpoints/subscription/create.ts b/packages/backend/src/server/api/endpoints/subscription/create.ts index eb1f59454087..2923dbb3566f 100644 --- a/packages/backend/src/server/api/endpoints/subscription/create.ts +++ b/packages/backend/src/server/api/endpoints/subscription/create.ts @@ -8,6 +8,7 @@ import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import { LoggerService } from "@/core/LoggerService.js"; export const meta = { tags: ['subscription'], @@ -46,6 +47,12 @@ export const meta = { id: 'f1b0a9f3-9f8a-4e8c-9b4d-0d2c1b7a9c0b', }, + statusInconsistency: { + message: 'The information registered in the payment service and the information stored on the server do not match.', + code: 'STATUS_INCONSENT', + id: 'f1d204e7-276a-4277-9e7b-14f5e038c2d8', + }, + unavailable: { message: 'Subscription unavailable.', code: 'UNAVAILABLE', @@ -75,8 +82,10 @@ export default class extends Endpoint { // eslint- private subscriptionPlansRepository: SubscriptionPlansRepository, private roleService: RoleService, private metaService: MetaService, + private loggerService: LoggerService, ) { super(meta, paramDef, async (ps, me) => { + const logger = this.loggerService.getLogger('subscription:create'); const instance = await this.metaService.fetch(true); if (!(instance.enableSubscriptions)) { throw new ApiError(meta.errors.unavailable); @@ -101,13 +110,22 @@ export default class extends Endpoint { // eslint- const stripe = new Stripe(this.config.stripe.secretKey); if (!userProfile.stripeCustomerId) { + const searchCustomer = await stripe.customers.search({ query: `email:"${userProfile.email}"` }); + if (searchCustomer.data.length !== 0) { + logger.info(`User with email ${userProfile.email} is already registered in Stripe but not recorded in UserProfile.`); + throw new ApiError(meta.errors.statusInconsistency); + } + const makeCustomer = await stripe.customers.create({ email: userProfile.email, + }, { + idempotencyKey: user.id, }); await this.userProfilesRepository.update({ userId: user.id }, { stripeCustomerId: makeCustomer.id, }); userProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + logger.info(`New Stripe customer created with ID ${makeCustomer.id} and associated with user ${user.id}`); } const subscriptionStatus = user.subscriptionStatus; @@ -136,6 +154,7 @@ export default class extends Endpoint { // eslint- for (const role of roles) { if (roleIds.includes(role.id)) { await this.roleService.unassign(user.id, role.id); + logger.info(`${user.id} has been unassigned the role "${role.id}".`); } } }); @@ -150,7 +169,8 @@ export default class extends Endpoint { // eslint- return; } - await stripe.subscriptionItems.update(subscriptionItem.id, { plan: plan.stripePriceId }); + await stripe.subscriptionItems.update(subscriptionItem.id, { plan: plan.stripePriceId }, { idempotencyKey: user.id + plan.id }); + logger.info(`Subscription plan changed for user ${user.id} to plan ${plan.id}`); return; } else { @@ -162,7 +182,7 @@ export default class extends Endpoint { // eslint- customer: userProfile.stripeCustomerId ?? undefined, allow_promotion_codes: true, return_url: `${this.config.url}/settings/subscription`, - }, {}); + }); return { redirect: {