Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const start = async (mongoTestClient?: MongoClient): Promise<FastifyInstance> =>
usersService,
licenseCodesRepository,
});
const objectStorageService = new ObjectStorageService(paymentService, envVariablesConfig, axios);
const objectStorageService = new ObjectStorageService();
const userFeaturesOverridesService = new UserFeaturesOverridesService(usersService, userFeatureOverridesRepository);
const productsService = new ProductsService(tiersService, usersService, userFeaturesOverridesService);

Expand Down
31 changes: 14 additions & 17 deletions src/services/objectStorage.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { PaymentService } from './payment.service';
import { sign } from 'jsonwebtoken';
import { Axios, AxiosRequestConfig } from 'axios';
import { type AppConfig } from '../config';
import axios, { AxiosRequestConfig } from 'axios';
import config from '../config';

function signToken(duration: string, secret: string) {
return sign({}, Buffer.from(secret, 'base64').toString('utf8'), {
Expand All @@ -11,11 +10,7 @@ function signToken(duration: string, secret: string) {
}

export class ObjectStorageService {
constructor(
private readonly paymentService: PaymentService,
private readonly config: AppConfig,
private readonly axios: Axios,
) {}
constructor() {}

async initObjectStorageUser(payload: { email: string; customerId: string }) {
const { email, customerId } = payload;
Expand All @@ -24,52 +19,52 @@ export class ObjectStorageService {
}

async reactivateAccount(payload: { customerId: string }): Promise<void> {
const jwt = signToken('5m', this.config.OBJECT_STORAGE_GATEWAY_SECRET);
const jwt = signToken('5m', config.OBJECT_STORAGE_GATEWAY_SECRET);
const params: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};

await this.axios.put(`${this.config.OBJECT_STORAGE_URL}/users/${payload.customerId}/reactivate`, {}, params);
await axios.put(`${config.OBJECT_STORAGE_URL}/users/${payload.customerId}/reactivate`, {}, params);
}

async suspendAccount(payload: { customerId: string }): Promise<void> {
const jwt = signToken('5m', this.config.OBJECT_STORAGE_GATEWAY_SECRET);
const jwt = signToken('5m', config.OBJECT_STORAGE_GATEWAY_SECRET);
const params: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};

await this.axios.put(`${this.config.OBJECT_STORAGE_URL}/users/${payload.customerId}/deactivate`, {}, params);
await axios.put(`${config.OBJECT_STORAGE_URL}/users/${payload.customerId}/deactivate`, {}, params);
}

async deleteAccount(payload: { customerId: string }): Promise<void> {
const jwt = signToken('5m', this.config.OBJECT_STORAGE_GATEWAY_SECRET);
const jwt = signToken('5m', config.OBJECT_STORAGE_GATEWAY_SECRET);
const params: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};

await this.axios.delete(`${this.config.OBJECT_STORAGE_URL}/users/${payload.customerId}`, params);
await axios.delete(`${config.OBJECT_STORAGE_URL}/users/${payload.customerId}`, params);
}

private async createUser(email: string, customerId: string): Promise<void> {
const jwt = signToken('5m', this.config.OBJECT_STORAGE_GATEWAY_SECRET);
const jwt = signToken('5m', config.OBJECT_STORAGE_GATEWAY_SECRET);
const params: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jwt}`,
},
};

await this.axios.post(
`${this.config.OBJECT_STORAGE_URL}/users`,
await axios.post(
`${config.OBJECT_STORAGE_URL}/users`,
{
email,
customerId,
Expand All @@ -78,3 +73,5 @@ export class ObjectStorageService {
);
}
}

export const objectStorageService = new ObjectStorageService();
15 changes: 6 additions & 9 deletions src/webhooks/events/ObjectStorageWebhookHandler.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import Stripe from 'stripe';
import { ObjectStorageService } from '../../services/objectStorage.service';
import { PaymentService } from '../../services/payment.service';
import { objectStorageService } from '../../services/objectStorage.service';
import { AxiosError, isAxiosError } from 'axios';
import Logger from '../../Logger';
import { paymentAdapter } from '../../infrastructure/payment.adapter';

export class ObjectStorageWebhookHandler {
constructor(
private readonly objectStorageService: ObjectStorageService,
private readonly paymentService: PaymentService,
) {}

/**
* Determines if the given product is an object storage product
* @param product The product object to check
Expand Down Expand Up @@ -48,15 +43,15 @@ export class ObjectStorageWebhookHandler {
return;
}

const product = await this.paymentService.getProduct(price.product as string);
const product = await paymentAdapter.getProduct(price.product as string);

if (!this.isObjectStorageProduct(product)) {
Logger.info(`Invoice ${invoice.id} for product ${price.product as string} is not an object-storage product`);
return;
}

try {
await this.objectStorageService.reactivateAccount({ customerId: customer.id });
await objectStorageService.reactivateAccount({ customerId: customer.id });
} catch (error) {
if (isAxiosError(error)) {
const axiosError = error as AxiosError;
Expand All @@ -78,3 +73,5 @@ export class ObjectStorageWebhookHandler {
);
}
}

export const objectStorageWebhookHandler = new ObjectStorageWebhookHandler();
8 changes: 2 additions & 6 deletions src/webhooks/events/invoices/InvoiceCompletedHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { FastifyBaseLogger } from 'fastify';
import { PaymentService } from '../../../services/payment.service';
import { PriceMetadata } from '../../../types/stripe';
import { User, UserType } from '../../../core/users/User';
import { ObjectStorageWebhookHandler } from '../ObjectStorageWebhookHandler';
import { TierNotFoundError, TiersService } from '../../../services/tiers.service';
import { UserNotFoundError, CouponNotBeingTrackedError, UsersService } from '../../../services/users.service';
import { StorageService } from '../../../services/storage.service';
import { NotFoundError } from '../../../errors/Errors';
import CacheService from '../../../services/cache.service';
import { Service, Tier } from '../../../core/users/Tier';
import Logger from '../../../Logger';
import { objectStorageWebhookHandler } from '../ObjectStorageWebhookHandler';

export interface InvoiceCompletedHandlerPayload {
customer: Stripe.Customer;
Expand All @@ -22,7 +22,6 @@ export interface InvoiceCompletedHandlerPayload {
export class InvoiceCompletedHandler {
private readonly logger: FastifyBaseLogger;
private readonly determineLifetimeConditions: DetermineLifetimeConditions;
private readonly objectStorageWebhookHandler: ObjectStorageWebhookHandler;
private readonly paymentService: PaymentService;
private readonly storageService: StorageService;
private readonly tiersService: TiersService;
Expand All @@ -32,7 +31,6 @@ export class InvoiceCompletedHandler {
constructor({
logger,
determineLifetimeConditions,
objectStorageWebhookHandler,
paymentService,
storageService,
tiersService,
Expand All @@ -41,7 +39,6 @@ export class InvoiceCompletedHandler {
}: {
logger: FastifyBaseLogger;
determineLifetimeConditions: DetermineLifetimeConditions;
objectStorageWebhookHandler: ObjectStorageWebhookHandler;
paymentService: PaymentService;
storageService: StorageService;
tiersService: TiersService;
Expand All @@ -50,7 +47,6 @@ export class InvoiceCompletedHandler {
}) {
this.logger = logger;
this.determineLifetimeConditions = determineLifetimeConditions;
this.objectStorageWebhookHandler = objectStorageWebhookHandler;
this.paymentService = paymentService;
this.storageService = storageService;
this.tiersService = tiersService;
Expand Down Expand Up @@ -94,7 +90,7 @@ export class InvoiceCompletedHandler {

if (isObjStoragePlan) {
Logger.info(`Invoice ${invoiceId} is for object storage, reactivating account if needed...`);
return this.objectStorageWebhookHandler.reactivateObjectStorageAccount(customer, invoice);
return objectStorageWebhookHandler.reactivateObjectStorageAccount(customer, invoice);
}

const tierBillingType = isLifetimePlan ? 'lifetime' : 'subscription';
Expand Down
47 changes: 47 additions & 0 deletions src/webhooks/events/invoices/InvoiceFailedHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Stripe from 'stripe';
import { paymentAdapter } from '../../../infrastructure/payment.adapter';
import { objectStorageService } from '../../../services/objectStorage.service';
import { UsersService } from '../../../services/users.service';
import Logger from '../../../Logger';

export class InvoiceFailedHandler {
constructor(private readonly usersService: UsersService) {}
async run(invoice: Stripe.Invoice) {
const customerId = invoice.customer as string;
const objectStorageLineItem = await this.findObjectStorageProduct(invoice);

if (objectStorageLineItem) {
Logger.info(
`Invoice ${invoice.id} for product ${objectStorageLineItem.price?.product as string} is an object-storage product`,
);
return objectStorageService.suspendAccount({ customerId });
}

const user = await this.usersService.findUserByCustomerID(customerId).catch(() => undefined);
if (user) {
Logger.info(`Drive payment failure notification sent for customer ${customerId} (user UUID: ${user.uuid})`);
return this.usersService.notifyFailedPayment(user.uuid);
}
}

async findObjectStorageProduct(invoice: Stripe.Invoice): Promise<Stripe.InvoiceLineItem | undefined> {
for (const line of invoice.lines.data) {
const price = line.price;
if (!price?.product) continue;
const productId = typeof price.product === 'string' ? price.product : price.product.id;

const product = await paymentAdapter.getProduct(productId);
if (this.isObjectStorageProduct(product)) return line;
}

return undefined;
}

isObjectStorageProduct(product: Stripe.Product | Stripe.DeletedProduct): product is Stripe.Product {
return (
(product as Stripe.Product).metadata &&
!!(product as Stripe.Product).metadata.type &&
(product as Stripe.Product).metadata.type === 'object-storage'
);
}
}
19 changes: 7 additions & 12 deletions src/webhooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ import { PaymentService } from '../services/payment.service';
import CacheService from '../services/cache.service';
import handleLifetimeRefunded from './handleLifetimeRefunded';
import { ObjectStorageService } from '../services/objectStorage.service';
import handleInvoicePaymentFailed from './handleInvoicePaymentFailed';
import { handleDisputeResult } from './handleDisputeResult';
import handleSetupIntentSucceeded from './handleSetupIntentSucceded';
import { TiersService } from '../services/tiers.service';
import handleFundsCaptured from './handleFundsCaptured';
import { DetermineLifetimeConditions } from '../core/users/DetermineLifetimeConditions';
import { ObjectStorageWebhookHandler } from './events/ObjectStorageWebhookHandler';
import { InvoiceCompletedHandler } from './events/invoices/InvoiceCompletedHandler';
import { BadRequestError } from '../errors/Errors';
import Logger from '../Logger';
import { InvoiceFailedHandler } from './events/invoices/InvoiceFailedHandler';

export default function (
stripe: Stripe,
Expand Down Expand Up @@ -53,15 +52,13 @@ export default function (
Logger.info(`Stripe event received: ${event.type}, id: ${event.id}`);

switch (event.type) {
case 'invoice.payment_failed':
await handleInvoicePaymentFailed(
event.data.object,
objectStorageService,
paymentService,
usersService,
fastify.log,
);
case 'invoice.payment_failed': {
const invoice = event.data.object;

const invoiceFailedHandler = new InvoiceFailedHandler(usersService);
await invoiceFailedHandler.run(invoice);
break;
}

case 'payment_intent.amount_capturable_updated':
await handleFundsCaptured(event.data.object, paymentService, objectStorageService, stripe, fastify.log);
Expand Down Expand Up @@ -123,11 +120,9 @@ export default function (
}

const determineLifetimeConditions = new DetermineLifetimeConditions(paymentService, tiersService);
const objectStorageWebhookHandler = new ObjectStorageWebhookHandler(objectStorageService, paymentService);
const handler = new InvoiceCompletedHandler({
logger: fastify.log,
determineLifetimeConditions,
objectStorageWebhookHandler,
paymentService,
storageService,
tiersService,
Expand Down
4 changes: 0 additions & 4 deletions src/webhooks/providers/bit2me/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { TiersService } from '../../../services/tiers.service';
import { BadRequestError } from '../../../errors/Errors';
import { InvoiceCompletedHandler } from '../../events/invoices/InvoiceCompletedHandler';
import { DetermineLifetimeConditions } from '../../../core/users/DetermineLifetimeConditions';
import { ObjectStorageWebhookHandler } from '../../events/ObjectStorageWebhookHandler';
import Logger from '../../../Logger';

export interface CryptoWebhookDependencies {
Expand Down Expand Up @@ -106,12 +105,9 @@ export default function ({

const determineLifetimeConditions = new DetermineLifetimeConditions(paymentService, tiersService);

const objectStorageWebhookHandler = new ObjectStorageWebhookHandler(objectStorageService, paymentService);

const handler = new InvoiceCompletedHandler({
logger: fastify.log,
determineLifetimeConditions,
objectStorageWebhookHandler,
paymentService,
cacheService,
tiersService,
Expand Down
9 changes: 0 additions & 9 deletions tests/src/helpers/services-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { PaymentService } from '../../../src/services/payment.service';
import { UsersService } from '../../../src/services/users.service';
import { StorageService } from '../../../src/services/storage.service';
import { Bit2MeService } from '../../../src/services/bit2me.service';
import { ObjectStorageService } from '../../../src/services/objectStorage.service';
import { ProductsService } from '../../../src/services/products.service';
import { LicenseCodesService } from '../../../src/services/licenseCodes.service';
import CacheService from '../../../src/services/cache.service';
Expand All @@ -20,7 +19,6 @@ import { UsersCouponsRepository } from '../../../src/core/coupons/UsersCouponsRe
import testFactory from '../utils/factory';
import config from '../../../src/config';
import { DetermineLifetimeConditions } from '../../../src/core/users/DetermineLifetimeConditions';
import { ObjectStorageWebhookHandler } from '../../../src/webhooks/events/ObjectStorageWebhookHandler';
import { InvoiceCompletedHandler } from '../../../src/webhooks/events/invoices/InvoiceCompletedHandler';
import { getLogger } from '../fixtures';
import { UserFeatureOverridesRepository } from '../../../src/core/users/MongoDBUserFeatureOverridesRepository';
Expand All @@ -33,12 +31,10 @@ export interface TestServices {
usersService: UsersService;
storageService: StorageService;
bit2MeService: Bit2MeService;
objectStorageService: ObjectStorageService;
productsService: ProductsService;
licenseCodesService: LicenseCodesService;
cacheService: CacheService;
determineLifetimeConditions: DetermineLifetimeConditions;
objectStorageWebhookHandler: ObjectStorageWebhookHandler;
invoiceCompletedHandler: InvoiceCompletedHandler;
userFeaturesOverridesService: UserFeaturesOverridesService;
}
Expand Down Expand Up @@ -101,13 +97,10 @@ export const createTestServices = (overrides: TestServiceOverrides = {}): TestSe
usersService,
licenseCodesRepository: repositories.licenseCodesRepository,
});
const objectStorageService = new ObjectStorageService(paymentService, config, axios);
const determineLifetimeConditions = new DetermineLifetimeConditions(paymentService, tiersService);
const objectStorageWebhookHandler = new ObjectStorageWebhookHandler(objectStorageService, paymentService);
const invoiceCompletedHandler = new InvoiceCompletedHandler({
logger: getLogger(),
determineLifetimeConditions,
objectStorageWebhookHandler,
paymentService,
storageService,
tiersService,
Expand All @@ -127,12 +120,10 @@ export const createTestServices = (overrides: TestServiceOverrides = {}): TestSe
usersService,
storageService,
bit2MeService,
objectStorageService,
productsService,
licenseCodesService,
cacheService,
determineLifetimeConditions,
objectStorageWebhookHandler,
invoiceCompletedHandler,
userFeaturesOverridesService,
...repositories,
Expand Down
Loading
Loading