diff --git a/src/billing/cloudpayments.ts b/src/billing/cloudpayments.ts index 0f54dee9..9d81b983 100644 --- a/src/billing/cloudpayments.ts +++ b/src/billing/cloudpayments.ts @@ -42,15 +42,9 @@ import { PaymentData } from './types/paymentData'; import cloudPaymentsApi from '../utils/cloudPaymentsApi'; import PlanModel from '../models/plan'; import { ClientApi, ClientService, CustomerReceiptItem, ReceiptApi, ReceiptTypes, TaxationSystem } from 'cloudpayments'; -import { ComposePaymentPayload } from './types/composePaymentPayload'; const PENNY_MULTIPLIER = 100; -interface ComposePaymentRequest extends express.Request { - query: ComposePaymentPayload & { [key: string]: any }; - context: import('../types/graphql').ResolverContextBase; -}; - /** * Class for describing the logic of payment routes */ @@ -86,7 +80,6 @@ export default class CloudPaymentsWebhooks { public getRouter(): express.Router { const router = express.Router(); - router.get('/compose-payment', this.composePayment.bind(this)); router.all('/check', this.check.bind(this)); router.all('/pay', this.pay.bind(this)); router.all('/fail', this.fail.bind(this)); @@ -95,120 +88,6 @@ export default class CloudPaymentsWebhooks { return router; } - /** - * Prepares payment data before charge - * - * @param req — Express request object - * @param res - Express response object - */ - private async composePayment(req: ComposePaymentRequest, res: express.Response): Promise { - const { workspaceId, tariffPlanId, shouldSaveCard } = req.query; - const userId = req.context.user.id; - - if (!workspaceId || !tariffPlanId || !userId) { - this.sendError(res, 1, `[Billing / Compose payment] No workspace, tariff plan or user id in request body -Details: -workspaceId: ${workspaceId} -tariffPlanId: ${tariffPlanId} -userId: ${userId}` - , req.query); - - return; - } - - let workspace; - let tariffPlan; - - try { - workspace = await this.getWorkspace(req, workspaceId); - tariffPlan = await this.getPlan(req, tariffPlanId); - } catch (e) { - const error = e as Error; - - this.sendError(res, 1, `[Billing / Compose payment] Can't get data from Database ${error.toString()}`, req.query); - - return; - } - - try { - await this.getMember(userId, workspace); - } catch (e) { - const error = e as Error; - - this.sendError(res, 1, `[Billing / Compose payment] Can't compose payment due to error: ${error.toString()}`, req.query); - - return; - } - const invoiceId = this.generateInvoiceId(tariffPlan, workspace); - - const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired(); - - // Calculate next payment date - const lastChargeDate = new Date(workspace.lastChargeDate); - const now = new Date(); - let nextPaymentDate: Date; - - if (isCardLinkOperation) { - nextPaymentDate = new Date(lastChargeDate); - } else { - nextPaymentDate = new Date(now); - } - - if (workspace.isDebug) { - nextPaymentDate.setDate(nextPaymentDate.getDate() + 1); - } else { - nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1); - } - - let checksum; - - try { - const checksumData = isCardLinkOperation ? { - isCardLinkOperation: true, - workspaceId: workspace._id.toString(), - userId: userId, - nextPaymentDate: nextPaymentDate.toISOString(), - } : { - workspaceId: workspace._id.toString(), - userId: userId, - tariffPlanId: tariffPlan._id.toString(), - shouldSaveCard: shouldSaveCard === 'true', - nextPaymentDate: nextPaymentDate.toISOString(), - }; - - checksum = await checksumService.generateChecksum(checksumData); - } catch (e) { - const error = e as Error; - - this.sendError(res, 1, `[Billing / Compose payment] Can't generate checksum: ${error.toString()}`, req.query); - - return; - } - - this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Compose payment] - -card link operation: ${isCardLinkOperation} -amount: ${+tariffPlan.monthlyCharge} RUB -last charge date: ${workspace.lastChargeDate?.toISOString()} -next payment date: ${nextPaymentDate.toISOString()} -workspace id: ${workspace._id.toString()} -debug: ${Boolean(workspace.isDebug)}` - , TelegramBotURLs.Money)); - - res.send({ - invoiceId, - plan: { - id: tariffPlan._id.toString(), - name: tariffPlan.name, - monthlyCharge: tariffPlan.monthlyCharge, - }, - isCardLinkOperation, - currency: 'RUB', - checksum, - nextPaymentDate: nextPaymentDate.toISOString(), - }); - } - /** * Generates invoice id for payment * diff --git a/src/billing/types/composePaymentPayload.ts b/src/billing/types/composePaymentPayload.ts deleted file mode 100644 index 91c3e1db..00000000 --- a/src/billing/types/composePaymentPayload.ts +++ /dev/null @@ -1,18 +0,0 @@ -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'; -} diff --git a/src/resolvers/billingNew.ts b/src/resolvers/billingNew.ts index c3647f53..c2aa1d2f 100644 --- a/src/resolvers/billingNew.ts +++ b/src/resolvers/billingNew.ts @@ -10,6 +10,8 @@ import { import checksumService from '../utils/checksumService'; import { UserInputError } from 'apollo-server-express'; import cloudPaymentsApi, { CloudPaymentsJsonData } from '../utils/cloudPaymentsApi'; +import * as telegram from '../utils/telegram'; +import { TelegramBotURLs } from '../utils/telegram'; /** * The amount we will debit to confirm the subscription. @@ -17,6 +19,17 @@ import cloudPaymentsApi, { CloudPaymentsJsonData } from '../utils/cloudPaymentsA */ const AMOUNT_FOR_CARD_VALIDATION = 1; +/** + * Input data for composePayment query + */ +interface ComposePaymentArgs { + input: { + workspaceId: string; + tariffPlanId: string; + shouldSaveCard?: boolean; + }; +} + /** * Data for processing payment with saved card */ @@ -58,6 +71,101 @@ export default { ): Promise { return factories.businessOperationsFactory.getWorkspacesBusinessOperations(ids); }, + + /** + * GraphQL version of composePayment: prepares data before charge + */ + async composePayment( + _obj: undefined, + { input }: ComposePaymentArgs, + { user, factories }: ResolverContextWithUser + ): Promise<{ + invoiceId: string; + plan: { id: string; name: string; monthlyCharge: number }; + isCardLinkOperation: boolean; + currency: string; + checksum: string; + nextPaymentDate: Date; + }> { + const { workspaceId, tariffPlanId, shouldSaveCard } = input; + + if (!workspaceId || !tariffPlanId || !user?.id) { + throw new UserInputError('No workspaceId, tariffPlanId or user id provided'); + } + + const workspace = await factories.workspacesFactory.findById(workspaceId); + const plan = await factories.plansFactory.findById(tariffPlanId); + + if (!workspace || !plan) { + throw new UserInputError("Can't get workspace or plan by provided ids"); + } + + const member = await workspace.getMemberInfo(user.id); + + if (!member) { + throw new UserInputError('User is not a member of the workspace'); + } + + const now = new Date(); + const invoiceId = `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${plan.name}`; + + const isCardLinkOperation = workspace.tariffPlanId.toString() === tariffPlanId && !workspace.isTariffPlanExpired(); + + // Calculate next payment date + const lastChargeDate = workspace.lastChargeDate ? new Date(workspace.lastChargeDate) : now; + const nextPaymentDate = isCardLinkOperation ? new Date(lastChargeDate) : new Date(now); + + if (workspace.isDebug) { + nextPaymentDate.setDate(nextPaymentDate.getDate() + 1); + } else { + nextPaymentDate.setMonth(nextPaymentDate.getMonth() + 1); + } + + const checksumData = isCardLinkOperation + ? { + isCardLinkOperation: true as const, + workspaceId: workspace._id.toString(), + userId: user.id, + nextPaymentDate: nextPaymentDate.toISOString(), + } + : { + workspaceId: workspace._id.toString(), + userId: user.id, + tariffPlanId: plan._id.toString(), + shouldSaveCard: Boolean(shouldSaveCard), + nextPaymentDate: nextPaymentDate.toISOString(), + }; + + const checksum = await checksumService.generateChecksum(checksumData); + + /** + * Send info to Telegram (non-blocking) + */ + telegram + .sendMessage(`✅ [Billing / Compose payment] + +card link operation: ${isCardLinkOperation} +amount: ${+plan.monthlyCharge} RUB +last charge date: ${workspace.lastChargeDate?.toISOString()} +next payment date: ${nextPaymentDate.toISOString()} +workspace id: ${workspace._id.toString()} +debug: ${Boolean(workspace.isDebug)}` + , TelegramBotURLs.Money) + .catch(e => console.error('Error while sending message to Telegram: ' + e)); + + return { + invoiceId, + plan: { + id: plan._id.toString(), + name: plan.name, + monthlyCharge: plan.monthlyCharge, + }, + isCardLinkOperation, + currency: 'RUB', + checksum, + nextPaymentDate, + }; + }, }, /** * Resolver for Union Payload type. diff --git a/src/typeDefs/billing.ts b/src/typeDefs/billing.ts index 16fcc7ef..12b3bc0a 100644 --- a/src/typeDefs/billing.ts +++ b/src/typeDefs/billing.ts @@ -34,6 +34,26 @@ type BillingSession { paymentURL: String! @renameFrom(name: "PaymentURL") } +""" +Minimal plan info used in composePayment response +""" +type ComposePaymentPlanInfo { + """ + Plan id in MongoDB + """ + id: ID! + + """ + Plan name + """ + name: String! + + """ + Monthly charge for plan + """ + monthlyCharge: Int! +} + """ User bank card """ @@ -197,11 +217,72 @@ input PayOnceInput { } +""" +Input for composePayment query +""" +input ComposePaymentInput { + """ + Workspace id for which the payment will be made + """ + workspaceId: ID! + + """ + Tariff plan id user is going to pay for + """ + tariffPlanId: ID! + + """ + Whether card should be saved for future recurrent payments + """ + shouldSaveCard: Boolean +} + +""" +Response of composePayment query +""" +type ComposePaymentResponse { + """ + Human-readable invoice identifier + """ + invoiceId: String! + + """ + Selected plan info + """ + plan: ComposePaymentPlanInfo! + + """ + True if only card linking validation payment is expected + """ + isCardLinkOperation: Boolean! + + """ + Currency code + """ + currency: String! + + """ + Checksum for subsequent payment verification + """ + checksum: String! + + """ + Next payment date (recurrent start) + """ + nextPaymentDate: DateTime! +} + + extend type Query { """ Get workspace billing history """ businessOperations("Workspaces IDs" ids: [ID!] = []): [BusinessOperation!]! @requireAuth @requireAdmin + + """ + Prepare payment data before charge (GraphQL version of composePayment) + """ + composePayment(input: ComposePaymentInput!): ComposePaymentResponse! @requireAuth } """