Skip to content

Commit 05b1b97

Browse files
Merge pull request #520 from codex-team/fix/check-auto-before-compose-payment
fix(payments): Rewrite composePayment with graphql
2 parents 055f47e + b13d481 commit 05b1b97

File tree

4 files changed

+189
-139
lines changed

4 files changed

+189
-139
lines changed

src/billing/cloudpayments.ts

Lines changed: 0 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,9 @@ import { PaymentData } from './types/paymentData';
4242
import cloudPaymentsApi from '../utils/cloudPaymentsApi';
4343
import PlanModel from '../models/plan';
4444
import { ClientApi, ClientService, CustomerReceiptItem, ReceiptApi, ReceiptTypes, TaxationSystem } from 'cloudpayments';
45-
import { ComposePaymentPayload } from './types/composePaymentPayload';
4645

4746
const PENNY_MULTIPLIER = 100;
4847

49-
interface ComposePaymentRequest extends express.Request {
50-
query: ComposePaymentPayload & { [key: string]: any };
51-
context: import('../types/graphql').ResolverContextBase;
52-
};
53-
5448
/**
5549
* Class for describing the logic of payment routes
5650
*/
@@ -86,7 +80,6 @@ export default class CloudPaymentsWebhooks {
8680
public getRouter(): express.Router {
8781
const router = express.Router();
8882

89-
router.get('/compose-payment', this.composePayment.bind(this));
9083
router.all('/check', this.check.bind(this));
9184
router.all('/pay', this.pay.bind(this));
9285
router.all('/fail', this.fail.bind(this));
@@ -95,120 +88,6 @@ export default class CloudPaymentsWebhooks {
9588
return router;
9689
}
9790

98-
/**
99-
* Prepares payment data before charge
100-
*
101-
* @param req — Express request object
102-
* @param res - Express response object
103-
*/
104-
private async composePayment(req: ComposePaymentRequest, res: express.Response): Promise<void> {
105-
const { workspaceId, tariffPlanId, shouldSaveCard } = req.query;
106-
const userId = req.context.user.id;
107-
108-
if (!workspaceId || !tariffPlanId || !userId) {
109-
this.sendError(res, 1, `[Billing / Compose payment] No workspace, tariff plan or user id in request body
110-
Details:
111-
workspaceId: ${workspaceId}
112-
tariffPlanId: ${tariffPlanId}
113-
userId: ${userId}`
114-
, req.query);
115-
116-
return;
117-
}
118-
119-
let workspace;
120-
let tariffPlan;
121-
122-
try {
123-
workspace = await this.getWorkspace(req, workspaceId);
124-
tariffPlan = await this.getPlan(req, tariffPlanId);
125-
} catch (e) {
126-
const error = e as Error;
127-
128-
this.sendError(res, 1, `[Billing / Compose payment] Can't get data from Database ${error.toString()}`, req.query);
129-
130-
return;
131-
}
132-
133-
try {
134-
await this.getMember(userId, workspace);
135-
} catch (e) {
136-
const error = e as Error;
137-
138-
this.sendError(res, 1, `[Billing / Compose payment] Can't compose payment due to error: ${error.toString()}`, req.query);
139-
140-
return;
141-
}
142-
const invoiceId = this.generateInvoiceId(tariffPlan, workspace);
143-
144-
const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired();
145-
146-
// Calculate next payment date
147-
const lastChargeDate = new Date(workspace.lastChargeDate);
148-
const now = new Date();
149-
let nextPaymentDate: Date;
150-
151-
if (isCardLinkOperation) {
152-
nextPaymentDate = new Date(lastChargeDate);
153-
} else {
154-
nextPaymentDate = new Date(now);
155-
}
156-
157-
if (workspace.isDebug) {
158-
nextPaymentDate.setDate(nextPaymentDate.getDate() + 1);
159-
} else {
160-
nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1);
161-
}
162-
163-
let checksum;
164-
165-
try {
166-
const checksumData = isCardLinkOperation ? {
167-
isCardLinkOperation: true,
168-
workspaceId: workspace._id.toString(),
169-
userId: userId,
170-
nextPaymentDate: nextPaymentDate.toISOString(),
171-
} : {
172-
workspaceId: workspace._id.toString(),
173-
userId: userId,
174-
tariffPlanId: tariffPlan._id.toString(),
175-
shouldSaveCard: shouldSaveCard === 'true',
176-
nextPaymentDate: nextPaymentDate.toISOString(),
177-
};
178-
179-
checksum = await checksumService.generateChecksum(checksumData);
180-
} catch (e) {
181-
const error = e as Error;
182-
183-
this.sendError(res, 1, `[Billing / Compose payment] Can't generate checksum: ${error.toString()}`, req.query);
184-
185-
return;
186-
}
187-
188-
this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Compose payment]
189-
190-
card link operation: ${isCardLinkOperation}
191-
amount: ${+tariffPlan.monthlyCharge} RUB
192-
last charge date: ${workspace.lastChargeDate?.toISOString()}
193-
next payment date: ${nextPaymentDate.toISOString()}
194-
workspace id: ${workspace._id.toString()}
195-
debug: ${Boolean(workspace.isDebug)}`
196-
, TelegramBotURLs.Money));
197-
198-
res.send({
199-
invoiceId,
200-
plan: {
201-
id: tariffPlan._id.toString(),
202-
name: tariffPlan.name,
203-
monthlyCharge: tariffPlan.monthlyCharge,
204-
},
205-
isCardLinkOperation,
206-
currency: 'RUB',
207-
checksum,
208-
nextPaymentDate: nextPaymentDate.toISOString(),
209-
});
210-
}
211-
21291
/**
21392
* Generates invoice id for payment
21493
*

src/billing/types/composePaymentPayload.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/resolvers/billingNew.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,26 @@ import {
1010
import checksumService from '../utils/checksumService';
1111
import { UserInputError } from 'apollo-server-express';
1212
import cloudPaymentsApi, { CloudPaymentsJsonData } from '../utils/cloudPaymentsApi';
13+
import * as telegram from '../utils/telegram';
14+
import { TelegramBotURLs } from '../utils/telegram';
1315

1416
/**
1517
* The amount we will debit to confirm the subscription.
1618
* After confirmation, we will refund the user money.
1719
*/
1820
const AMOUNT_FOR_CARD_VALIDATION = 1;
1921

22+
/**
23+
* Input data for composePayment query
24+
*/
25+
interface ComposePaymentArgs {
26+
input: {
27+
workspaceId: string;
28+
tariffPlanId: string;
29+
shouldSaveCard?: boolean;
30+
};
31+
}
32+
2033
/**
2134
* Data for processing payment with saved card
2235
*/
@@ -58,6 +71,101 @@ export default {
5871
): Promise<BusinessOperationModel[]> {
5972
return factories.businessOperationsFactory.getWorkspacesBusinessOperations(ids);
6073
},
74+
75+
/**
76+
* GraphQL version of composePayment: prepares data before charge
77+
*/
78+
async composePayment(
79+
_obj: undefined,
80+
{ input }: ComposePaymentArgs,
81+
{ user, factories }: ResolverContextWithUser
82+
): Promise<{
83+
invoiceId: string;
84+
plan: { id: string; name: string; monthlyCharge: number };
85+
isCardLinkOperation: boolean;
86+
currency: string;
87+
checksum: string;
88+
nextPaymentDate: Date;
89+
}> {
90+
const { workspaceId, tariffPlanId, shouldSaveCard } = input;
91+
92+
if (!workspaceId || !tariffPlanId || !user?.id) {
93+
throw new UserInputError('No workspaceId, tariffPlanId or user id provided');
94+
}
95+
96+
const workspace = await factories.workspacesFactory.findById(workspaceId);
97+
const plan = await factories.plansFactory.findById(tariffPlanId);
98+
99+
if (!workspace || !plan) {
100+
throw new UserInputError("Can't get workspace or plan by provided ids");
101+
}
102+
103+
const member = await workspace.getMemberInfo(user.id);
104+
105+
if (!member) {
106+
throw new UserInputError('User is not a member of the workspace');
107+
}
108+
109+
const now = new Date();
110+
const invoiceId = `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${plan.name}`;
111+
112+
const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired();
113+
114+
// Calculate next payment date
115+
const lastChargeDate = workspace.lastChargeDate ? new Date(workspace.lastChargeDate) : now;
116+
const nextPaymentDate = isCardLinkOperation ? new Date(lastChargeDate) : new Date(now);
117+
118+
if (workspace.isDebug) {
119+
nextPaymentDate.setDate(nextPaymentDate.getDate() + 1);
120+
} else {
121+
nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1);
122+
}
123+
124+
const checksumData = isCardLinkOperation
125+
? {
126+
isCardLinkOperation: true as const,
127+
workspaceId: workspace._id.toString(),
128+
userId: user.id,
129+
nextPaymentDate: nextPaymentDate.toISOString(),
130+
}
131+
: {
132+
workspaceId: workspace._id.toString(),
133+
userId: user.id,
134+
tariffPlanId: plan._id.toString(),
135+
shouldSaveCard: Boolean(shouldSaveCard),
136+
nextPaymentDate: nextPaymentDate.toISOString(),
137+
};
138+
139+
const checksum = await checksumService.generateChecksum(checksumData);
140+
141+
/**
142+
* Send info to Telegram (non-blocking)
143+
*/
144+
telegram
145+
.sendMessage(`✅ [Billing / Compose payment]
146+
147+
card link operation: ${isCardLinkOperation}
148+
amount: ${+plan.monthlyCharge} RUB
149+
last charge date: ${workspace.lastChargeDate?.toISOString()}
150+
next payment date: ${nextPaymentDate.toISOString()}
151+
workspace id: ${workspace._id.toString()}
152+
debug: ${Boolean(workspace.isDebug)}`
153+
, TelegramBotURLs.Money)
154+
.catch(e => console.error('Error while sending message to Telegram: ' + e));
155+
156+
return {
157+
invoiceId,
158+
plan: {
159+
id: plan._id.toString(),
160+
name: plan.name,
161+
monthlyCharge: plan.monthlyCharge,
162+
},
163+
isCardLinkOperation,
164+
currency: 'RUB',
165+
checksum,
166+
nextPaymentDate,
167+
};
168+
},
61169
},
62170
/**
63171
* Resolver for Union Payload type.

src/typeDefs/billing.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ type BillingSession {
3434
paymentURL: String! @renameFrom(name: "PaymentURL")
3535
}
3636
37+
"""
38+
Minimal plan info used in composePayment response
39+
"""
40+
type ComposePaymentPlanInfo {
41+
"""
42+
Plan id in MongoDB
43+
"""
44+
id: ID!
45+
46+
"""
47+
Plan name
48+
"""
49+
name: String!
50+
51+
"""
52+
Monthly charge for plan
53+
"""
54+
monthlyCharge: Int!
55+
}
56+
3757
"""
3858
User bank card
3959
"""
@@ -197,11 +217,72 @@ input PayOnceInput {
197217
}
198218
199219
220+
"""
221+
Input for composePayment query
222+
"""
223+
input ComposePaymentInput {
224+
"""
225+
Workspace id for which the payment will be made
226+
"""
227+
workspaceId: ID!
228+
229+
"""
230+
Tariff plan id user is going to pay for
231+
"""
232+
tariffPlanId: ID!
233+
234+
"""
235+
Whether card should be saved for future recurrent payments
236+
"""
237+
shouldSaveCard: Boolean
238+
}
239+
240+
"""
241+
Response of composePayment query
242+
"""
243+
type ComposePaymentResponse {
244+
"""
245+
Human-readable invoice identifier
246+
"""
247+
invoiceId: String!
248+
249+
"""
250+
Selected plan info
251+
"""
252+
plan: ComposePaymentPlanInfo!
253+
254+
"""
255+
True if only card linking validation payment is expected
256+
"""
257+
isCardLinkOperation: Boolean!
258+
259+
"""
260+
Currency code
261+
"""
262+
currency: String!
263+
264+
"""
265+
Checksum for subsequent payment verification
266+
"""
267+
checksum: String!
268+
269+
"""
270+
Next payment date (recurrent start)
271+
"""
272+
nextPaymentDate: DateTime!
273+
}
274+
275+
200276
extend type Query {
201277
"""
202278
Get workspace billing history
203279
"""
204280
businessOperations("Workspaces IDs" ids: [ID!] = []): [BusinessOperation!]! @requireAuth @requireAdmin
281+
282+
"""
283+
Prepare payment data before charge (GraphQL version of composePayment)
284+
"""
285+
composePayment(input: ComposePaymentInput!): ComposePaymentResponse! @requireAuth
205286
}
206287
207288
"""

0 commit comments

Comments
 (0)