Skip to content
Merged
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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.1.6",
"version": "1.1.7",
"main": "index.ts",
"license": "UNLICENSED",
"scripts": {
Expand Down
72 changes: 61 additions & 11 deletions src/billing/cloudpayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@
import cloudPaymentsApi from '../utils/cloudPaymentsApi';
import PlanModel from '../models/plan';
import { ClientApi, ClientService, CustomerReceiptItem, ReceiptApi, ReceiptTypes, TaxationSystem } from 'cloudpayments';
import { ComposePaymentPayload } from './types/composePaymentPayload';

/**
* Custom data of the plan prolongation request
*/
type PlanProlongationData = PlanProlongationPayload & PaymentData;
interface ComposePaymentRequest extends express.Request {
query: ComposePaymentPayload & { [key: string]: any };

Check warning on line 49 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type
context: import('../types/graphql').ResolverContextBase;
};

/**
* Class for describing the logic of payment routes
Expand Down Expand Up @@ -99,8 +100,8 @@
* @param req — Express request object
* @param res - Express response object
*/
private async composePayment(req: express.Request, res: express.Response): Promise<void> {
const { workspaceId, tariffPlanId, shouldSaveCard } = req.query as Record<string, string>;
private async composePayment(req: ComposePaymentRequest, res: express.Response): Promise<void> {
const { workspaceId, tariffPlanId, shouldSaveCard } = req.query;
const userId = req.context.user.id;

if (!workspaceId || !tariffPlanId || !userId) {
Expand Down Expand Up @@ -134,15 +135,23 @@
}
const invoiceId = this.generateInvoiceId(tariffPlan, workspace);

const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !this.isPlanExpired(workspace);

let checksum;

try {
checksum = await checksumService.generateChecksum({
const checksumData = isCardLinkOperation ? {
isCardLinkOperation: true,
workspaceId: workspace._id.toString(),
userId: userId,
} : {
workspaceId: workspace._id.toString(),
userId: userId,
tariffPlanId: tariffPlan._id.toString(),
shouldSaveCard: shouldSaveCard === 'true',
});
};

checksum = await checksumService.generateChecksum(checksumData);
} catch (e) {
const error = e as Error;

Expand All @@ -158,12 +167,33 @@
name: tariffPlan.name,
monthlyCharge: tariffPlan.monthlyCharge,
},
isCardLinkOperation,
currency: 'RUB',
checksum,
});
}

/**

Check warning on line 176 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Returns true if workspace's plan is expired
* @param workspace - workspace to check
*/
private isPlanExpired(workspace: WorkspaceModel): boolean {
const lastChargeDate = new Date(workspace.lastChargeDate);

let planExpiracyDate;

if (workspace.isDebug) {
planExpiracyDate = lastChargeDate.setDate(lastChargeDate.getDate() + 1);
} else {
planExpiracyDate = lastChargeDate.setMonth(lastChargeDate.getMonth() + 1);
}

const isPlanExpired = planExpiracyDate < Date.now();

return isPlanExpired;
}

/**

Check warning on line 196 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Generates invoice id for payment
*
* @param tariffPlan - tariff plan to generate invoice id
Expand Down Expand Up @@ -201,7 +231,13 @@
let member: ConfirmedMemberDBScheme;
let plan: PlanDBScheme;

if (!data.workspaceId || !data.tariffPlanId || !data.userId) {
if (data.isCardLinkOperation && (!data.userId || !data.workspaceId)) {
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] Card linking – invalid data', body);

return;
}

if (!data.isCardLinkOperation && (!data.userId || !data.workspaceId || !data.tariffPlanId)) {
this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the request', body);

return;
Expand Down Expand Up @@ -235,6 +271,18 @@
return;
}

if (data.isCardLinkOperation) {
telegram
.sendMessage(`✅ [Billing / Check] Card linked for subscription workspace «${workspace.name}»`, TelegramBotURLs.Money)
.catch(e => console.error('Error while sending message to Telegram: ' + e));

res.json({
code: CheckCodes.SUCCESS,
} as CheckResponse);

return;
}

/**
* Create business operation about creation of subscription
*/
Expand Down Expand Up @@ -266,7 +314,8 @@

telegram.sendMessage(`✅ [Billing / Check] All checks passed successfully «${workspace.name}»`, TelegramBotURLs.Money)
.catch(e => console.error('Error while sending message to Telegram: ' + e));

HawkCatcher.send(new Error('[Billing / Check] All checks passed successfully'), body as any);

Check warning on line 318 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type

res.json({
code: CheckCodes.SUCCESS,
Expand Down Expand Up @@ -544,7 +593,7 @@

this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Fail] Transaction failed for «${workspace.name}»`, TelegramBotURLs.Money));

HawkCatcher.send(new Error('[Billing / Fail] Transaction failed'), body as any);

Check warning on line 596 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type

res.json({
code: FailCodes.SUCCESS,
Expand Down Expand Up @@ -705,7 +754,7 @@
* @param errorText - error description
* @param backtrace - request data and error data
*/
private sendError(res: express.Response, errorCode: CheckCodes | PayCodes | FailCodes | RecurrentCodes, errorText: string, backtrace: { [key: string]: any }): void {

Check warning on line 757 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type
res.json({
code: errorCode,
});
Expand All @@ -720,9 +769,9 @@
*
* @param req - request with necessary data
*/
private async getDataFromRequest(req: express.Request): Promise<PlanProlongationData> {
private async getDataFromRequest(req: express.Request): Promise<PaymentData> {
const context = req.context;
const body: CheckRequest = req.body;
const body: CheckRequest | PayRequest | FailRequest = req.body;

/**
* If Data is not presented in body means there is a recurring payment
Expand Down Expand Up @@ -752,6 +801,7 @@
tariffPlanId: workspace.tariffPlanId.toString(),
userId,
shouldSaveCard: false,
isCardLinkOperation: false,
};
}

Expand All @@ -766,7 +816,7 @@
promise.catch(e => console.error('Error while sending message to Telegram: ' + e));
}

/**

Check warning on line 819 in src/billing/cloudpayments.ts

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Parses body and returns card data
* @param request - request body to parse
*/
Expand Down
18 changes: 18 additions & 0 deletions src/billing/types/composePaymentPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface ComposePaymentPayload {
/**
* Workspace Identifier
*/
workspaceId: string;
/**
* Id of the user making the payment
*/
userId: string;
/**
* Workspace current plan id or plan id to change
*/
tariffPlanId: string;
/**
* If true, we will save user card
*/
shouldSaveCard: 'true' | 'false';
}
20 changes: 20 additions & 0 deletions src/billing/types/paymentData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,24 @@ export interface PaymentData {
* Data for Cloudpayments needs
*/
cloudPayments?: CloudPaymentsSettings;
/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need these fields?

* Workspace Identifier
*/
workspaceId: string;
/**
* Id of the user making the payment
*/
userId: string;
/**
* Workspace current plan id or plan id to change
*/
tariffPlanId: string;
/**
* If true, we will save user card
*/
shouldSaveCard: boolean;
/**
* True if this is card linking operation – charging minimal amount of money to validate card info
*/
isCardLinkOperation: boolean;
}
5 changes: 5 additions & 0 deletions src/resolvers/billingNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ export default {
*/
async payWithCard(_obj: undefined, args: PayWithCardArgs, { factories, user }: ResolverContextWithUser): Promise<any> {
const paymentData = checksumService.parseAndVerifyChecksum(args.input.checksum);

if (!('tariffPlanId' in paymentData)) {
throw new UserInputError('Invalid checksum');
}

const fullUserInfo = await factories.usersFactory.findById(user.id);

const workspace = await factories.workspacesFactory.findById(paymentData.workspaceId);
Expand Down
67 changes: 53 additions & 14 deletions src/utils/checksumService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import { PlanProlongationPayload } from '@hawk.so/types';
import jwt, { Secret } from 'jsonwebtoken';

export type ChecksumData = PlanPurchaseChecksumData | CardLinkChecksumData;

interface PlanPurchaseChecksumData {
/**
* Workspace Identifier
*/
workspaceId: string;
/**
* Id of the user making the payment
*/
userId: string;
/**
* Workspace current plan id or plan id to change
*/
tariffPlanId: string;
/**
* If true, we will save user card
*/
shouldSaveCard: boolean;
}

interface CardLinkChecksumData {
/**
* Workspace Identifier
*/
workspaceId: string;
/**
* Id of the user making the payment
*/
userId: string;
/**
* True if this is card linking operation – charging minimal amount of money to validate card info
*/
isCardLinkOperation: boolean;
}

/**
* Helper class for working with checksums
*/
Expand All @@ -10,7 +46,7 @@ class ChecksumService {
*
* @param data - data for processing billing request
*/
public async generateChecksum(data: PlanProlongationPayload): Promise<string> {
public async generateChecksum(data: ChecksumData): Promise<string> {
return jwt.sign(
data,
process.env.JWT_SECRET_BILLING_CHECKSUM as Secret,
Expand All @@ -23,20 +59,23 @@ class ChecksumService {
*
* @param checksum - checksum to parse
*/
public parseAndVerifyChecksum(checksum: string): PlanProlongationPayload {
const payload = jwt.verify(checksum, process.env.JWT_SECRET_BILLING_CHECKSUM as Secret) as PlanProlongationPayload;

/**
* Filter unnecessary fields from JWT payload (e.g. "iat")
*/
const { tariffPlanId, workspaceId, userId, shouldSaveCard } = payload;
public parseAndVerifyChecksum(checksum: string): ChecksumData {
const payload = jwt.verify(checksum, process.env.JWT_SECRET_BILLING_CHECKSUM as Secret) as ChecksumData;

return {
tariffPlanId,
workspaceId,
userId,
shouldSaveCard,
};
if ('isCardLinkOperation' in payload) {
return {
workspaceId: payload.workspaceId,
userId: payload.userId,
isCardLinkOperation: payload.isCardLinkOperation,
};
} else {
return {
workspaceId: payload.workspaceId,
userId: payload.userId,
tariffPlanId: payload.tariffPlanId,
shouldSaveCard: payload.shouldSaveCard,
};
}
}
}

Expand Down
Loading