diff --git a/.env.sample b/.env.sample index 1e3ff48e..fc3de809 100644 --- a/.env.sample +++ b/.env.sample @@ -30,6 +30,19 @@ PLAYGROUND_ENABLE=false # AMQP URL AMQP_URL=amqp://guest:guest@rabbitmq +# Billing settings +BILLING_DEBUG=true +BILLING_COMPANY_EMAIL="team@hawk.so" + +### Accounting module ### +# Accounting service URL +# CODEX_ACCOUNTING_URL=http://accounting:3999/graphql + +# Files with certs +TLS_CA_CERT= +TLS_CERT= +TLS_KEY= + ## GitHub OAuth app client ID GITHUB_CLIENT_ID=fakedata @@ -51,6 +64,18 @@ HAWK_CATCHER_TOKEN= ## Telegram bot url to send log messages TELEGRAM_MAIN_CHAT_URL= +## Telegam bot url for operations with money +TELEGRAM_MONEY_CHAT_URL= + +# Cloudpayments public id +CLOUDPAYMENTS_PUBLIC_ID=test_api_00000000000000000000001 + +# Cloudpayments secret string +CLOUDPAYMENTS_SECRET= + +# INN of legal entity for CloudKassir +LEGAL_ENTITY_INN= + # Token for Amplitude analytics AMPLITUDE_TOKEN= diff --git a/.env.test b/.env.test index 07f83c40..5d1b2dcd 100644 --- a/.env.test +++ b/.env.test @@ -44,7 +44,7 @@ SMTP_SENDER_NAME= SMTP_SENDER_ADDRESS= # AMQP URL -AMQP_URL= +AMQP_URL=amqp://guest:guest@rabbitmq:5672/ # Billing settings BILLING_DEBUG=true @@ -52,7 +52,7 @@ BILLING_COMPANY_EMAIL="team@hawk.so" ### Accounting module ### # Accounting service URL -CODEX_ACCOUNTING_URL= +# CODEX_ACCOUNTING_URL= # Enable or disable tls verify TLS_VERIFY=true @@ -100,4 +100,4 @@ AWS_S3_ACCESS_KEY_ID= AWS_S3_SECRET_ACCESS_KEY= AWS_S3_BUCKET_NAME= AWS_S3_BUCKET_BASE_URL= -AWS_S3_BUCKET_ENDPOINT= \ No newline at end of file +AWS_S3_BUCKET_ENDPOINT= diff --git a/.eslintrc.js b/.eslintrc.js index 1fe24f29..00f90d1c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,5 +3,13 @@ module.exports = { env: { 'node': true, 'jest': true + }, + rules: { + '@typescript-eslint/camelcase': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/explicit-function-return-type': 'warn', + 'require-jsdoc': 'warn', + 'no-shadow': 'warn', + 'no-unused-expressions': 'warn' } }; diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index ad5b0955..31d6db85 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -39,9 +39,9 @@ jobs: - uses: actions/checkout@v2 # Setup node environment - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: - node-version: 15 + node-version-file: '.nvmrc' registry-url: https://registry.npmjs.org/ # Bump version to the next prerelease (patch) with rc suffix diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..9f895130 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,14 @@ +name: Run integration tests on push + +on: + - push + +jobs: + tests: + name: Run integration tests + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Run tests + run: yarn test:integration diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 34d856cc..5805844f 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Use Node.js 12.x - uses: actions/setup-node@v1 + - name: Use Node.js + uses: actions/setup-node@v3 with: - node-version: 12.x + node-version-file: '.nvmrc' - run: yarn install - run: yarn lint-test diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..ac51dae6 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,63 @@ +version: "3.4" +services: + api: + build: + dockerfile: "./docker/Dockerfile.dev" + context: . + user: "node" + env_file: + - ./test/integration/api.env + volumes: + - ./:/usr/src/app + - /usr/src/app/node_modules + - ./test/integration/api.env:/usr/src/app/.env + depends_on: + - mongodb + - rabbitmq + # - accounting + stdin_open: true + tty: true + + mongodb: + image: mongo:4.2.13 + volumes: + - mongodata-test:/data/db + + tests: + build: + dockerfile: "./docker/Dockerfile.dev" + context: . + depends_on: + rabbitmq: + condition: service_healthy + api: + condition: service_started + command: dockerize -wait http://api:4000/.well-known/apollo/server-health -timeout 30s yarn jest --config=./test/integration/jest.config.js --runInBand test/integration + volumes: + - ./:/usr/src/app + - /usr/src/app/node_modules + + rabbitmq: + image: rabbitmq:3-management + ports: + - 15672:15672 + - 5672:5672 + volumes: + - ./test/integration/rabbit.definitions.json:/tmp/rabbit.definitions.json:ro + environment: + - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbitmq_management load_definitions "/tmp/rabbit.definitions.json" + healthcheck: + test: ["CMD-SHELL", "rabbitmqctl status || exit 1"] + interval: 5s + timeout: 3s + retries: 5 + + # accounting: + # image: codexteamuser/codex-accounting:prod + # env_file: + # - ./test/integration/accounting.env + # volumes: + # - ./test/integration/accounting.env:/usr/src/app/.env + +volumes: + mongodata-test: diff --git a/migrations/20241110143438-add-monthlyChargeCurrency.js b/migrations/20241110143438-add-monthlyChargeCurrency.js new file mode 100644 index 00000000..d33952ad --- /dev/null +++ b/migrations/20241110143438-add-monthlyChargeCurrency.js @@ -0,0 +1,45 @@ +module.exports = { + async up(db, client) { + /** + * Use one transaction for all requests + */ + const session = client.startSession(); + + try { + await session.withTransaction(async () => { + const plansCollection = db.collection('plans'); + + await plansCollection.updateMany( + {}, + { $set: { monthlyChargeCurrency: 'RUB' } } // Set the default value for monthlyChargeCurrency + ); + }); + + } finally { + await session.endSession(); + } + }, + + async down(db, client) { + /** + * Use one transaction for all requests + */ + const session = client.startSession(); + + try { + + await session.withTransaction(async () => { + const plansCollection = db.collection('plans'); + + await plansCollection.updateMany( + {}, + { $unset: { monthlyChargeCurrency: "" } } // Remove the monthlyChargeCurrency field + ); + + }); + + } finally { + await session.endSession(); + } + } +}; diff --git a/migrations/20241110161019-add-businessOperations-currency.js b/migrations/20241110161019-add-businessOperations-currency.js new file mode 100644 index 00000000..620c7313 --- /dev/null +++ b/migrations/20241110161019-add-businessOperations-currency.js @@ -0,0 +1,44 @@ +module.exports = { + async up(db, client) { + /** + * Use one transaction for all requests + */ + const session = client.startSession(); + + try { + await session.withTransaction(async () => { + const businessOperationsCollection = db.collection('businessOperations'); + + await businessOperationsCollection.updateMany( + {}, + { $set: { 'payload.currency': 'RUB' } } // Set the default value for payload.currency + ); + }); + + } finally { + await session.endSession(); + } + }, + + async down(db, client) { + /** + * Use one transaction for all requests + */ + const session = client.startSession(); + + try { + + await session.withTransaction(async () => { + const businessOperationsCollection = db.collection('businessOperations'); + + await businessOperationsCollection.updateMany( + {}, + { $unset: { 'payload.businessOperations': "" } } // Remove the businessOperations field + ); + }); + + } finally { + await session.endSession(); + } + } +}; diff --git a/package.json b/package.json index 91016128..303b0073 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hawk.api", - "version": "1.0.23", + "version": "1.1.0", "main": "index.ts", "license": "UNLICENSED", "scripts": { @@ -14,14 +14,18 @@ "migrations:create": "docker-compose exec api yarn migrate-mongo create", "migrations:up": "docker-compose exec api yarn migrate-mongo up", "migrations:down": "docker-compose exec api yarn migrate-mongo down", - "test": "jest --coverage" + "test": "jest --coverage", + "test:integration": "docker compose -f docker-compose.test.yml up --build --exit-code-from tests tests", + "test:integration:down": "docker compose -f docker-compose.test.yml down --volumes" }, "devDependencies": { + "@shelf/jest-mongodb": "^1.2.2", "@types/jest": "^26.0.8", "eslint": "^6.7.2", "eslint-config-codex": "1.2.4", "eslint-plugin-import": "^2.19.1", "jest": "^26.2.2", + "mongodb-memory-server": "^6.6.1", "nodemon": "^2.0.2", "ts-jest": "^26.1.4", "ts-node": "^10.9.1", @@ -33,7 +37,7 @@ "@graphql-tools/schema": "^8.5.1", "@graphql-tools/utils": "^8.9.0", "@hawk.so/nodejs": "^3.1.1", - "@hawk.so/types": "^0.1.18", + "@hawk.so/types": "^0.1.21", "@types/amqp-connection-manager": "^2.0.4", "@types/bson": "^4.0.5", "@types/debug": "^4.1.5", @@ -53,6 +57,8 @@ "axios": "^0.27.2", "body-parser": "^1.19.0", "bson": "^4.6.5", + "cloudpayments": "^5.0.4", + "codex-accounting-sdk": "https://github.com/codex-team/codex-accounting-sdk.git", "dataloader": "^2.0.0", "dotenv": "^16.0.1", "escape-html": "^1.0.3", diff --git a/src/billing/cloudpayments.ts b/src/billing/cloudpayments.ts new file mode 100644 index 00000000..ff41abe3 --- /dev/null +++ b/src/billing/cloudpayments.ts @@ -0,0 +1,818 @@ +import express from 'express'; +import * as telegram from '../utils/telegram'; +import { TelegramBotURLs } from '../utils/telegram'; +import { + CheckCodes, + CheckRequest, + CheckResponse, + FailCodes, + FailRequest, + FailResponse, + PayCodes, + PayRequest, + PayResponse, + RecurrentCodes, + RecurrentRequest, + RecurrentResponse +} from './types'; +import { ReasonCodesTranscript, SubscriptionStatus } from './types/enums'; +import { + BankCard, + BusinessOperationStatus, + BusinessOperationType, + ConfirmedMemberDBScheme, + PayloadOfWorkspacePlanPurchase, + PlanDBScheme, + PlanProlongationPayload +} from '@hawk.so/types'; +import { PENNY_MULTIPLIER } from 'codex-accounting-sdk'; +import WorkspaceModel from '../models/workspace'; +import HawkCatcher from '@hawk.so/nodejs'; +import { publish } from '../rabbitmq'; +import sendNotification from '../utils/personalNotifications'; +import { + PaymentFailedNotificationTask, + PaymentSuccessNotificationTask, + SenderWorkerTaskType +} from '../types/userNotifications'; +import BusinessOperationModel from '../models/businessOperation'; +import UserModel from '../models/user'; +import checksumService from '../utils/checksumService'; +import { WebhookData } from './types/request'; +import { PaymentData } from './types/paymentData'; +import cloudPaymentsApi from '../utils/cloudPaymentsApi'; +import PlanModel from '../models/plan'; +import { ClientApi, ClientService, CustomerReceiptItem, ReceiptApi, ReceiptTypes, TaxationSystem } from 'cloudpayments'; + +/** + * Custom data of the plan prolongation request + */ +type PlanProlongationData = PlanProlongationPayload & PaymentData; + +/** + * Class for describing the logic of payment routes + */ +export default class CloudPaymentsWebhooks { + private readonly clientService = new ClientService({ + publicId: process.env.CLOUDPAYMENTS_PUBLIC_ID || '', + privateKey: process.env.CLOUDPAYMENTS_SECRET || '', + }) + + /** + * Receipt API instance to call receipt methods + */ + private readonly receiptApi: ReceiptApi; + + /** + * Client API instance to call CloundPayments API + */ + private readonly clientApi: ClientApi; + + /** + * Creates class instance + */ + constructor() { + this.receiptApi = this.clientService.getReceiptApi(); + this.clientApi = this.clientService.getClientApi(); + } + + /** + * Returns router for payments + * + * @returns - express router for payments + */ + 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)); + router.all('/recurrent', this.recurrent.bind(this)); + + return router; + } + + /** + * Prepares payment data before charge + * + * @param req — Express request object + * @param res - Express response object + */ + private async composePayment(req: express.Request, res: express.Response): Promise { + const { workspaceId, tariffPlanId, shouldSaveCard } = req.query as Record; + 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`, 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); + + let checksum; + + try { + checksum = await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: userId, + tariffPlanId: tariffPlan._id.toString(), + shouldSaveCard: shouldSaveCard === 'true', + }); + } catch (e) { + const error = e as Error; + + this.sendError(res, 1, `[Billing / Compose payment] Can't generate checksum: ${error.toString()}`, req.query); + + return; + } + + res.send({ + invoiceId, + plan: { + id: tariffPlan._id.toString(), + name: tariffPlan.name, + monthlyCharge: tariffPlan.monthlyCharge, + }, + currency: 'RUB', + checksum, + }); + } + + /** + * Generates invoice id for payment + * + * @param tariffPlan - tariff plan to generate invoice id + * @param workspace - workspace data to generate invoice id + */ + private generateInvoiceId(tariffPlan: PlanDBScheme, workspace: WorkspaceModel): string { + const now = new Date(); + + return `${workspace.name} ${now.getDate()}/${now.getMonth() + 1} ${tariffPlan.name}`; + } + + /** + * Route to confirm the correctness of a user's payment + * https://developers.cloudpayments.ru/#check + * + * @param req - cloudpayments request with payment details + * @param res - check result code + */ + private async check(req: express.Request, res: express.Response): Promise { + const context = req.context; + const body: CheckRequest = req.body; + let data; + + try { + data = await this.getDataFromRequest(req); + } catch (e) { + const error = e as Error; + + this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, `[Billing / Check] Invalid request: ${error.toString()}`, body); + + return; + } + + let workspace: WorkspaceModel; + let member: ConfirmedMemberDBScheme; + let plan: PlanDBScheme; + + if (!data.workspaceId || !data.tariffPlanId || !data.userId) { + this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, '[Billing / Check] There is no necessary data in the request', body); + + return; + } + + const { workspaceId, userId, tariffPlanId } = data; + + try { + workspace = await this.getWorkspace(req, workspaceId); + member = await this.getMember(userId, workspace); + plan = await this.getPlan(req, tariffPlanId); + } catch (e) { + const error = e as Error; + + this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, `[Billing / Check] ${error.toString()}`, body); + + return; + } + + const recurrentPaymentSettings = data.cloudPayments?.recurrent; + + /** + * The amount will be considered correct if it is equal to the cost of the tariff plan. + * Also, the cost will be correct if it is a payment to activate the subscription. + */ + const isRightAmount = +body.Amount === plan.monthlyCharge || recurrentPaymentSettings?.startDate; + + if (!isRightAmount) { + this.sendError(res, CheckCodes.WRONG_AMOUNT, `[Billing / Check] Amount does not equal to plan monthly charge`, body); + + return; + } + + /** + * Create business operation about creation of subscription + */ + try { + await context.factories.businessOperationsFactory.create({ + transactionId: body.TransactionId.toString(), + type: BusinessOperationType.WorkspacePlanPurchase, + status: BusinessOperationStatus.Pending, + payload: { + workspaceId: workspace._id, + amount: +body.Amount * PENNY_MULTIPLIER, + currency: body.Currency, + userId: member._id, + tariffPlanId: plan._id, + }, + dtCreated: new Date(), + }); + } catch (err) { + const error = err as Error; + + this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, `[Billing / Check] Business operation wasn't created: ${error.toString()}`, body); + + res.json({ + code: CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, + } as CheckResponse); + + return; + } + + 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); + + res.json({ + code: CheckCodes.SUCCESS, + } as CheckResponse); + } + + /** + * Route for fixing a successful payment + * https://developers.cloudpayments.ru/#pay + * + * @param req - cloudpayments request with payment details + * @param res - result code + */ + private async pay(req: express.Request, res: express.Response): Promise { + const body: PayRequest = req.body; + let data; + + try { + data = await this.getDataFromRequest(req); + } catch (e) { + const error = e as Error; + + this.sendError(res, CheckCodes.SUCCESS, `[Billing / Pay] Invalid request: ${error.toString()}`, body); + + return; + } + + if (!data.workspaceId || !data.tariffPlanId || !data.userId) { + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] No workspace, tariff plan or user id in request body`, body); + + return; + } + + let businessOperation; + let workspace; + let tariffPlan; + let user; + + try { + businessOperation = await this.getBusinessOperation(req, body.TransactionId.toString()); + workspace = await this.getWorkspace(req, data.workspaceId); + tariffPlan = await this.getPlan(req, data.tariffPlanId); + user = await this.getUser(req, data.userId); + } catch (e) { + const error = e as Error; + + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Can't get data from Database ${error.toString()}`, body); + + return; + } + + try { + await businessOperation.setStatus(BusinessOperationStatus.Confirmed); + await workspace.resetBillingPeriod(); + await workspace.changePlan(tariffPlan._id); + + const subscriptionId = body.SubscriptionId; + + /** + * Cancellation of the current subscription if: + * 1) the user pays manually (the workspace has an active subscription, but the request body does not) + * 2) if payment is made for another subscription (subscriptions id are not equal) + */ + if (workspace.subscriptionId) { + if (!subscriptionId || subscriptionId !== workspace.subscriptionId) { + await this.clientApi.cancelSubscription({ + Id: workspace.subscriptionId, + }); + } + } + + if (subscriptionId) { + await workspace.setSubscriptionId(subscriptionId); + } + } catch (e) { + const error = e as Error; + + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Can't update workspace billing data ${error.toString()}`, body); + + return; + } + + // let accountId = workspace.accountId; + + /* + * try { + * if (!workspace.accountId) { + * accountId = (await context.accounting.createAccount({ + * name: `WORKSPACE:${workspace.name}`, + * type: AccountType.LIABILITY, + * currency: Currency.RUB, + * })).recordId; + * await workspace.setAccountId(accountId); + * } + */ + + /* + * await context.accounting.payOnce({ + * accountId: accountId, + * amount: tariffPlan.monthlyCharge * PENNY_MULTIPLIER, + * description: `Account replenishment to pay for the tariff plan with id ${tariffPlan._id}. CloudPayments transaction ID: ${body.TransactionId}`, + * }); + */ + + /* + * await context.accounting.purchase({ + * accountId, + * amount: tariffPlan.monthlyCharge * PENNY_MULTIPLIER, + * description: `Charging for tariff plan with id ${tariffPlan._id}. CloudPayments transaction ID: ${body.TransactionId}`, + * }); + * } catch (e) { + * const error = e as Error; + */ + + // this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while creating operations in accounting ${error.toString()}`, body); + + /* + * return; + * } + */ + + try { + await publish('cron-tasks', 'cron-tasks/limiter', JSON.stringify({ + type: 'check-single-workspace', + workspaceId: data.workspaceId, + })); + } catch (e) { + const error = e as Error; + + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while sending task to limiter worker ${error.toString()}`, body); + + return; + } + + try { + // todo: add plan-prolongation notification if it was a payment by subscription + const senderWorkerTask: PaymentSuccessNotificationTask = { + type: SenderWorkerTaskType.PaymentSuccess, + payload: { + workspaceId: data.workspaceId, + tariffPlanId: data.tariffPlanId, + userId: data.userId, + }, + }; + + await sendNotification(user, senderWorkerTask); + } catch (e) { + const error = e as Error; + + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while sending notification to the user ${error.toString()}`, body); + + return; + } + + try { + if (data.shouldSaveCard) { + const cardData = this.getCardData(body); + + if (cardData) { + await user.saveNewBankCard(cardData); + } + } + } catch (e) { + const error = e as Error; + + this.sendError(res, PayCodes.SUCCESS, `[Billing / Pay] Error while saving user card: ${error.toString()}`, body); + + return; + } + + try { + /** + * Cancel payment if it is deferred + */ + if (data.cloudPayments?.recurrent?.startDate) { + this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Pay] Recurrent payments activated for «${workspace.name}». 1 RUB charged`, TelegramBotURLs.Money)); + await cloudPaymentsApi.cancelPayment(body.TransactionId); + this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Pay] Recurrent payments activated for «${workspace.name}». 1 RUB returned`, TelegramBotURLs.Money)); + } else { + /** + * Russia code from ISO 3166-1 + */ + const RUSSIA_ISO_CODE = 'RU'; + + /** + * Send receipt only in case that user pays from russian card + */ + const userEmail = body.IssuerBankCountry === RUSSIA_ISO_CODE ? user.email : undefined; + + await this.sendReceipt(workspace, tariffPlan, userEmail); + + this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Pay] Payment passed successfully for «${workspace.name}»`, TelegramBotURLs.Money)); + } + } catch (e) { + const error = e as Error; + + this.sendError(res, PayCodes.SUCCESS, error.toString(), body); + + return; + } + + res.json({ + code: PayCodes.SUCCESS, + } as PayResponse); + } + + /** + * Route for refused payments + * https://developers.cloudpayments.ru/#fail + * + * @param req - cloudpayments request with payment details + * @param res - result code + */ + private async fail(req: express.Request, res: express.Response): Promise { + const body: FailRequest = req.body; + let data: PlanProlongationPayload; + + try { + data = await this.getDataFromRequest(req); + } catch (e) { + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] Invalid request`, body); + + return; + } + + let businessOperation; + let workspace; + let user; + + if (!data.workspaceId || !data.userId || !data.tariffPlanId) { + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] No workspace or user id or plan id in request body`, body); + + return; + } + + try { + businessOperation = await this.getBusinessOperation(req, body.TransactionId.toString()); + workspace = await this.getWorkspace(req, data.workspaceId); + user = await this.getUser(req, data.userId); + } catch (e) { + const error = e as Error; + + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] ${error.toString()}`, body); + + return; + } + + try { + await businessOperation.setStatus(BusinessOperationStatus.Rejected); + } catch (e) { + const error = e as Error; + + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] Can't update business operation status ${error.toString()}`, body); + + return; + } + + try { + const senderWorkerTask: PaymentFailedNotificationTask = { + type: SenderWorkerTaskType.PaymentFailed, + payload: { + workspaceId: data.workspaceId, + reason: ReasonCodesTranscript[body.ReasonCode], + }, + }; + + await sendNotification(user, senderWorkerTask); + } catch (e) { + const error = e as Error; + + this.sendError(res, FailCodes.SUCCESS, `[Billing / Fail] Error while sending notification to the user ${error.toString()}`, body); + + return; + } + + this.handleSendingToTelegramError(telegram.sendMessage(`✅ [Billing / Fail] Transaction failed for «${workspace.name}»`, TelegramBotURLs.Money)); + + HawkCatcher.send(new Error('[Billing / Fail] Transaction failed'), body as any); + + res.json({ + code: FailCodes.SUCCESS, + } as FailResponse); + } + + /** + * Route is executed if the status of the recurring payment subscription has been changed. + * https://developers.cloudpayments.ru/#recurrent + * + * @param req - cloudpayments request with subscription details + * @param res - result code + */ + private async recurrent(req: express.Request, res: express.Response): Promise { + const body: RecurrentRequest = req.body; + const context = req.context; + + this.handleSendingToTelegramError(telegram.sendMessage(`[Billing / Recurrent] New recurrent event with ${body.Status} status`, TelegramBotURLs.Money)); + HawkCatcher.send(new Error(`[Billing / Recurrent] New recurrent event with ${body.Status} status`), req.body); + + switch (body.Status) { + case SubscriptionStatus.CANCELLED: + case SubscriptionStatus.REJECTED: { + let workspace; + + try { + workspace = await context.factories.workspacesFactory.findBySubscriptionId(body.Id); + } catch (e) { + const error = e as Error; + + this.sendError(res, RecurrentCodes.SUCCESS, `[Billing / Recurrent] Can't get data from database: ${error.toString()}`, { + body, + workspace, + }); + + return; + } + + if (!workspace) { + return; + } + + try { + await workspace.setSubscriptionId(null); + } catch (e) { + const error = e as Error; + + this.sendError(res, RecurrentCodes.SUCCESS, `[Billing / Recurrent] Can't remove subscriptionId from workspace: ${error.toString()}`, { + body, + workspace, + }); + } + } + } + + res.json({ + code: RecurrentCodes.SUCCESS, + } as RecurrentResponse); + } + + /** + * Get workspace by workspace id + * + * @param req - express request + * @param workspaceId - id of workspace + */ + private async getWorkspace(req: express.Request, workspaceId: string): Promise { + const workspace = await req.context.factories.workspacesFactory.findById(workspaceId); + + if (!workspace) { + throw new Error('Workspace not found'); + } + + return workspace; + } + + /** + * Get user by its id + * + * @param req - express request + * @param userId - id of user to fetch + */ + private async getUser(req: express.Request, userId: string): Promise { + const user = await req.context.factories.usersFactory.findById(userId); + + if (!user) { + throw new Error('User not found'); + } + + return user; + } + + /** + * Get business operation by transaction id + * + * @param req - express request + * @param transactionId - id of the transaction for fetching business operation + */ + private async getBusinessOperation(req: express.Request, transactionId: string): Promise { + const businessOperation = await req.context.factories.businessOperationsFactory.getBusinessOperationByTransactionId(transactionId); + + if (!businessOperation) { + throw new Error('Business operation not found'); + } + + return businessOperation; + } + + /** + * Get member info + * + * @param userId - id of current user + * @param workspace - workspace data + */ + private async getMember(userId: string, workspace: WorkspaceModel): Promise { + const user = await workspace.getMemberInfo(userId); + + if (!user) { + throw new Error('User not found'); + } + + if (!user || WorkspaceModel.isPendingMember(user)) { + throw new Error('User cannot pay for current workspace because he is not a member of it'); + } + + if (!user.isAdmin) { + throw new Error('User cannot pay for current workspace because he is not an admin'); + } + + return user; + } + + /** + * Get workspace plan + * + * @param req - express request + * @param tariffPlanId - plan id + */ + private async getPlan(req: express.Request, tariffPlanId: string): Promise { + const plan = await req.context.factories.plansFactory.findById(tariffPlanId); + + if (!plan) { + throw new Error('Plan not found'); + } + + return plan; + } + + /** + * Send an error to telegram, Hawk and send an express response with the error code + * + * @param res - Express response + * @param errorCode - code of error + * @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 { + res.json({ + code: errorCode, + }); + + this.handleSendingToTelegramError(telegram.sendMessage(`❌ ${errorText}`, TelegramBotURLs.Money)); + + HawkCatcher.send(new Error(errorText), backtrace); + } + + /** + * Parses request body and returns data from it + * + * @param req - request with necessary data + */ + private async getDataFromRequest(req: express.Request): Promise { + const context = req.context; + const body: CheckRequest = req.body; + + /** + * If Data is not presented in body means there is a recurring payment + * Data field is presented only in one-time payment requests or subscription initial request + */ + if (body.Data) { + const parsedData = JSON.parse(body.Data || '{}') as WebhookData; + + return { + ...checksumService.parseAndVerifyChecksum(parsedData.checksum), + ...parsedData, + }; + } + + const subscriptionId = body.SubscriptionId; + const userId = body.AccountId; + + if (!subscriptionId || !userId) { + throw new Error('Invalid request: no subscription or user id'); + } + + const workspace = await context.factories.workspacesFactory.findBySubscriptionId(subscriptionId); + + if (workspace) { + return { + workspaceId: workspace._id.toString(), + tariffPlanId: workspace.tariffPlanId.toString(), + userId, + shouldSaveCard: false, + }; + } + + throw new Error('Invalid request: no necessary data'); + } + + /** + * Wrapper for telegram promise + * @param promise - promise to handle + */ + private handleSendingToTelegramError(promise: Promise): void { + promise.catch(e => console.error('Error while sending message to Telegram: ' + e)); + } + + /** + * Parses body and returns card data + * @param request - request body to parse + */ + private getCardData(request: PayRequest): Omit | null { + if (!request.CardType || !request.CardExpDate || !request.CardLastFour || !request.CardFirstSix || !request.Token) { + return null; + } + + return { + cardExpDate: request.CardExpDate, + firstSix: +request.CardFirstSix, + lastFour: +request.CardLastFour, + token: request.Token, + type: request.CardType, + }; + } + + /** + * Send receipt to user after successful payment + * + * @param workspace - workspace for which payment is made + * @param tariff - paid tariff plan + * @param userMail - user email address + */ + private async sendReceipt(workspace: WorkspaceModel, tariff: PlanModel, userMail?: string): Promise { + /** + * A general tax that applies to all commercial activities + * involving the production and distribution of goods and the provision of services + * Also known as "НДС" in Russia + */ + const VALUE_ADDED_TAX = 20; + + const item: CustomerReceiptItem = { + amount: tariff.monthlyCharge, + label: `${tariff.name} tariff plan`, + price: tariff.monthlyCharge, + // vat: VALUE_ADDED_TAX, + quantity: 1, + }; + + await this.receiptApi.createReceipt( + { + Type: ReceiptTypes.Income, + Inn: Number(process.env.LEGAL_ENTITY_INN), + }, + { + Items: [ item ], + email: userMail, + taxationSystem: TaxationSystem.GENERAL, + } + ); + } +} diff --git a/src/billing/index.ts b/src/billing/index.ts new file mode 100644 index 00000000..fbe27090 --- /dev/null +++ b/src/billing/index.ts @@ -0,0 +1,20 @@ +import CloudPaymentsWebhooks from './cloudpayments'; +import express from 'express'; +import cors from 'cors'; + +/** + * Hawk billing + */ +export default class Billing { + /** + * Append billing routes to the express app + * + * @param app - express app + */ + public appendRoutes(app: express.Application): void { + const providerWebhooks = new CloudPaymentsWebhooks(); + + app.use(cors()); + app.use('/billing', providerWebhooks.getRouter()); + } +} diff --git a/src/billing/types/cardDetails.ts b/src/billing/types/cardDetails.ts new file mode 100644 index 00000000..ae41a5bb --- /dev/null +++ b/src/billing/types/cardDetails.ts @@ -0,0 +1,31 @@ +import { CardType } from './enums'; + +/** + * Data based on IP CloudPayments request + */ +export interface CardDetails { + /** + * First 6 digits of the card number + */ + CardFirstSix: string; + + /** + * Last 4 digits of the card number + */ + CardLastFour: string; + + /** + * Card payment system: Visa, MasterCard, Maestro or MIR + */ + CardType: CardType; + + /** + * Card expiration date in MM/YY format + */ + CardExpDate: string; + + /** + * Card token for repeated payments without entering details + */ + Token?: string; +} diff --git a/src/billing/types/enums/cardType.ts b/src/billing/types/enums/cardType.ts new file mode 100644 index 00000000..9b275752 --- /dev/null +++ b/src/billing/types/enums/cardType.ts @@ -0,0 +1,10 @@ +/** + * Possible card types + */ +export enum CardType { + VISA = 'Visa', + MASTERCARD = 'Mastercard', + MAESTRO = 'Maestro', + MIR = 'МИР', + UNION_PAY = 'UnionPay' +} diff --git a/src/billing/types/enums/currency.ts b/src/billing/types/enums/currency.ts new file mode 100644 index 00000000..0ceb8566 --- /dev/null +++ b/src/billing/types/enums/currency.ts @@ -0,0 +1,7 @@ +/** + * Payment currency + */ +export enum Currency { + USD = 'USD', + RUB = 'RUB' +} diff --git a/src/billing/types/enums/index.ts b/src/billing/types/enums/index.ts new file mode 100644 index 00000000..6dd92499 --- /dev/null +++ b/src/billing/types/enums/index.ts @@ -0,0 +1,8 @@ +export { Currency } from './currency'; +export { CardType } from './cardType'; +export { OperationType } from './operationType'; +export { OperationStatus } from './operationStatus'; +export { SubscriptionStatus } from './subscriptionStatus'; +export { ReasonCode } from './reasonCode'; +export { Interval } from './interval'; +export { ReasonCodesTranscript } from './reasonCodeTranscript'; diff --git a/src/billing/types/enums/interval.ts b/src/billing/types/enums/interval.ts new file mode 100644 index 00000000..60929ad0 --- /dev/null +++ b/src/billing/types/enums/interval.ts @@ -0,0 +1,8 @@ +/** + * Reccurent payments interval + */ +export enum Interval { + WEEK = 'Week', + MONTH = 'Month', + DAY = 'Day' +} diff --git a/src/billing/types/enums/operationStatus.ts b/src/billing/types/enums/operationStatus.ts new file mode 100644 index 00000000..f86c38e0 --- /dev/null +++ b/src/billing/types/enums/operationStatus.ts @@ -0,0 +1,14 @@ +/** + * Payment status in case of successful completion + */ +export enum OperationStatus { + /** + * Status for one-step payments, + */ + COMPLETED = 'Completed', + + /** + * Status for two-step payments + */ + AUTHORIZED = 'Authorized' +} \ No newline at end of file diff --git a/src/billing/types/enums/operationType.ts b/src/billing/types/enums/operationType.ts new file mode 100644 index 00000000..516285cb --- /dev/null +++ b/src/billing/types/enums/operationType.ts @@ -0,0 +1,19 @@ +/** + * Operation type + */ +export enum OperationType { + /** + * Payment operation + */ + PAYMENT = 'Payment', + + /** + * Refund operation + */ + REFUND = 'Refund', + + /** + * Payout to card + */ + CARD_PAYOUT = 'CardPayout' +} \ No newline at end of file diff --git a/src/billing/types/enums/reasonCode.ts b/src/billing/types/enums/reasonCode.ts new file mode 100644 index 00000000..6847fdeb --- /dev/null +++ b/src/billing/types/enums/reasonCode.ts @@ -0,0 +1,160 @@ +/** + * Transaction rejection code + * https://developers.cloudpayments.ru/#kody-oshibok + */ +export enum ReasonCode { + /** + * Refusal of the issuer to conduct an online transaction + */ + REFER_TO_CARD_ISSUER = 5001, + + /** + * Refusal of the issuer to conduct an online transaction + */ + INVALID_MERCHANT = 5003, + + /** + * Card lost + */ + PICK_UP_CARD = 5004, + + /** + * Refusal of the issuer without explanation + */ + DO_NOT_HONOR = 5005, + + /** + * Network refusal to carry out the operation or incorrect CVV code + */ + ERROR = 5006, + + /** + * Card lost + */ + PICK_UP_CARD_SPECIAL_CONDITIONS = 5007, + + /** + * The card is not available for online payments + */ + INVALID_TRANSACTION = 5012, + + /** + * Too small or too large transaction amount + */ + AMOUNT_ERROR = 5013, + + /** + * Incorrect card number + */ + INVALID_CARD_NUMBER = 5014, + + /** + * Issuer not found + */ + NO_SUCH_ISSUER = 5015, + + /** + * Refusal of the issuer without explanation + */ + TRANSACTION_ERROR = 5019, + + /** + * Error on the acquirer's side - the transaction was incorrectly formed + */ + FORMAT_ERROR = 5030, + + /** + * Unknown card issuer + */ + BANK_NOT_SUPPORTED_BY_SWITCH = 5031, + + /** + * Lost card has expired + */ + EXPIRED_CARD_PICKUP = 5033, + + /** + * Issuer refusal - suspicion of fraud + */ + SUSPECTED_FRAUD = 5034, + + /** + * The card is not intended for payments + */ + RESTRICTED_CARD = 5036, + + /** + * Card lost + */ + LOST_CARD = 5041, + + /** + * Card stolen + */ + STOLEN_CARD = 5043, + + /** + * Insufficient funds + */ + INSUFFICIENT_FUNDS = 5051, + + /** + * The card is expired or the expiration date is incorrect + */ + TRANSACTION_NOT_PERMITTED = 5057, + + /** + * Restriction on the card + */ + RESTRICTED_CARD_2 = 5062, + + /** + * Card blocked due to security breaches + */ + SECURITY_VIOLATION = 5063, + + /** + * The limit of card transactions has been exceeded + */ + EXCEED_WITHDRAWAL_FREQUENCY = 5065, + + /** + * Invalid CVV code + */ + INCORRECT_CVV = 5082, + + /** + * Issuer unavailable + */ + TIMEOUT = 5091, + + /** + * Issuer unavailable + */ + CANNOT_REACH_NETWORK = 5092, + + /** + * Acquiring bank or network error + */ + SYSTEM_ERROR = 5096, + + /** + * The transaction cannot be processed for other reasons + */ + UNABLE_TO_PROCESS = 5204, + + /** + * 3-D Secure authorization failed + */ + AUTHENTICATION_FAILED = 5206, + + /** + * 3-D Secure authorization not available + */ + AUTHENTICATION_UNAVAILABLE = 5207, + + /** + * Acquiring limits for transactions + */ + ANTI_FRAUD = 5300 +} diff --git a/src/billing/types/enums/reasonCodeTranscript.ts b/src/billing/types/enums/reasonCodeTranscript.ts new file mode 100644 index 00000000..bbb92441 --- /dev/null +++ b/src/billing/types/enums/reasonCodeTranscript.ts @@ -0,0 +1,163 @@ +import { ReasonCode } from './reasonCode'; + +/** + * Transcript of transaction rejection code for payment failed event + * Transcript will be used in notification after words "because of" + * https://developers.cloudpayments.ru/#kody-oshibok + */ +export const ReasonCodesTranscript = { + /** + * Refusal of the issuer to conduct an online transaction + */ + [ReasonCode.REFER_TO_CARD_ISSUER]: 'issuer refuse to conduct an online transaction', + + /** + * Refusal of the issuer to conduct an online transaction + */ + [ReasonCode.INVALID_MERCHANT]: 'issuer refuse to conduct an online transaction', + + /** + * Card lost + */ + [ReasonCode.PICK_UP_CARD]: 'the card was lost', + + /** + * Refusal of the issuer without explanation + */ + [ReasonCode.DO_NOT_HONOR]: 'error on the payment service side', + + /** + * Network refusal to carry out the operation or incorrect CVV code + */ + [ReasonCode.ERROR]: 'network refusal to carry out the operation or incorrect CVV code', + + /** + * Card lost + */ + [ReasonCode.PICK_UP_CARD_SPECIAL_CONDITIONS]: 'the card was lost', + + /** + * The card is not available for online payments + */ + [ReasonCode.INVALID_TRANSACTION]: 'the card is not available for online payments', + + /** + * Too small or too large transaction amount + */ + [ReasonCode.AMOUNT_ERROR]: 'too small or too large transaction amount', + + /** + * Incorrect card number + */ + [ReasonCode.INVALID_CARD_NUMBER]: 'incorrect card number', + + /** + * Issuer not found + */ + [ReasonCode.NO_SUCH_ISSUER]: 'issuer not found', + + /** + * Refusal of the issuer without explanation + */ + [ReasonCode.TRANSACTION_ERROR]: 'error on the payment service side', + + /** + * Error on the acquirer's side - the transaction was incorrectly formed + */ + [ReasonCode.FORMAT_ERROR]: 'error on the payment service side', + + /** + * Unknown card issuer + */ + [ReasonCode.BANK_NOT_SUPPORTED_BY_SWITCH]: 'unknown card issuer', + + /** + * Lost card has expired + */ + [ReasonCode.EXPIRED_CARD_PICKUP]: 'the card has expired', + + /** + * Issuer refusal - suspicion of fraud + */ + [ReasonCode.SUSPECTED_FRAUD]: 'error on the payment service side', + + /** + * The card is not intended for payments + */ + [ReasonCode.RESTRICTED_CARD]: 'the card is not intended for payments', + + /** + * Card lost + */ + [ReasonCode.LOST_CARD]: 'error on the payment service side', + + /** + * Card stolen + */ + [ReasonCode.STOLEN_CARD]: 'error on the payment service side', + + /** + * Insufficient funds + */ + [ReasonCode.INSUFFICIENT_FUNDS]: 'insufficient funds', + + /** + * The card is expired or the expiration date is incorrect + */ + [ReasonCode.TRANSACTION_NOT_PERMITTED]: 'the card has expired or the expiration date is incorrect', + + /** + * Restriction on the card + */ + [ReasonCode.RESTRICTED_CARD_2]: 'restriction on the card', + + /** + * Card blocked due to security breaches + */ + [ReasonCode.SECURITY_VIOLATION]: 'the card blocked due to security breaches', + + /** + * The limit of card transactions has been exceeded + */ + [ReasonCode.EXCEED_WITHDRAWAL_FREQUENCY]: 'the limit of card transactions has been exceeded', + + /** + * Invalid CVV code + */ + [ReasonCode.INCORRECT_CVV]: 'invalid CVV code', + + /** + * Issuer unavailable + */ + [ReasonCode.TIMEOUT]: 'issuer unavailable', + + /** + * Issuer unavailable + */ + [ReasonCode.CANNOT_REACH_NETWORK]: 'issuer unavailable', + + /** + * Acquiring bank or network error + */ + [ReasonCode.SYSTEM_ERROR]: 'error on the payment service side', + + /** + * The transaction cannot be processed for other reasons + */ + [ReasonCode.UNABLE_TO_PROCESS]: 'error on the payment service side', + + /** + * 3-D Secure authorization failed + */ + [ReasonCode.AUTHENTICATION_FAILED]: '3-D Secure authorization failed', + + /** + * 3-D Secure authorization not available + */ + [ReasonCode.AUTHENTICATION_UNAVAILABLE]: '3-D Secure authorization not available', + + /** + * Acquiring limits for transactions + */ + [ReasonCode.ANTI_FRAUD]: 'acquiring limits for transactions', +}; diff --git a/src/billing/types/enums/subscriptionStatus.ts b/src/billing/types/enums/subscriptionStatus.ts new file mode 100644 index 00000000..2422bf7e --- /dev/null +++ b/src/billing/types/enums/subscriptionStatus.ts @@ -0,0 +1,34 @@ +/** + * Possible subscription status + */ +export enum SubscriptionStatus { + /** + * Subscription active. + * After creation and next successful payment + */ + ACTIVE = 'Active', + + /** + * Subscription expired. + * After one or two consecutive unsuccessful payment attempts + */ + PASTDUE = 'PastDue', + + /** + * Subscription cancelled. + * In case of cancellation upon request + */ + CANCELLED = 'Cancelled', + + /** + * Subscription rejected. + * In case of three unsuccessful payment attempts in a row + */ + REJECTED = 'Rejected', + + /** + * Subscription expired. + * In case of completion of the maximum number of periods (if specified) + */ + EXPIRED = 'Expired' +} diff --git a/src/billing/types/index.ts b/src/billing/types/index.ts new file mode 100644 index 00000000..cad8cea0 --- /dev/null +++ b/src/billing/types/index.ts @@ -0,0 +1,2 @@ +export { CheckCodes, CheckResponse, PayCodes, PayResponse, FailCodes, FailResponse, RecurrentCodes, RecurrentResponse } from './response'; +export { CheckRequest, PayRequest, FailRequest, RecurrentRequest } from './request'; diff --git a/src/billing/types/ipData.ts b/src/billing/types/ipData.ts new file mode 100644 index 00000000..47eff304 --- /dev/null +++ b/src/billing/types/ipData.ts @@ -0,0 +1,29 @@ +/** + * Data based on IP CloudPayments request + */ +export interface IpData { + /** + * Payer's IP address + */ + IpAdress?: string; + + /** + * ISO3166-1 two-letter country code of the payer's country + */ + IpCountry?: string; + + /** + * Payer's city + */ + IpCity?: string; + + /** + * Payer's region + */ + IpRegion?: string; + + /** + * Payer's district + */ + IpDistrict?: string; +} diff --git a/src/billing/types/paymentData.ts b/src/billing/types/paymentData.ts new file mode 100644 index 00000000..9ca61185 --- /dev/null +++ b/src/billing/types/paymentData.ts @@ -0,0 +1,43 @@ +/** + * Data for setting up recurring payments + */ +interface RecurrentPaymentSettings { + /** + * Payment interval + */ + interval: 'Day' | 'Week' | 'Month'; + + /** + * Payment period. That is, how often to withdraw money + */ + period: number; + + /** + * Subscription start date (first payment) + */ + startDate?: string; + + /** + * Recurring payment amount. + */ + amount?: number; +} + +/** + * Data for the needs of Cloudpayments + */ +interface CloudPaymentsSettings { + /** + * Data for recurrent payments + * + * @see https://developers.cloudpayments.ru/#rekurrentnye-platezhi-podpiska + */ + recurrent: RecurrentPaymentSettings; +} + +export interface PaymentData { + /** + * Data for Cloudpayments needs + */ + cloudPayments?: CloudPaymentsSettings; +} diff --git a/src/billing/types/request.ts b/src/billing/types/request.ts new file mode 100644 index 00000000..93039d75 --- /dev/null +++ b/src/billing/types/request.ts @@ -0,0 +1,421 @@ +import { Currency, OperationType, OperationStatus, SubscriptionStatus, ReasonCode, Interval } from './enums'; +import { CardDetails } from './cardDetails'; +import { IpData } from './ipData'; +import { PaymentData } from './paymentData'; + +/** + * Check request body + * https://developers.cloudpayments.ru/#check + */ +export interface CheckRequest extends CardDetails, IpData { + /** + * Number of transaction in the system + */ + TransactionId: number; + + /** + * Payment amount from the payment parameters + */ + Amount: string; + + /** + * Currency: RUB/USD + */ + Currency: Currency; + + /** + * Date/time of payment creation in UTC time zone + */ + DateTime: Date; + + /** + * Test mode sign + */ + TestMode: boolean; + + /** + * Payment status in case of successful completion: + * Completed - for one-step payments, + * Authorized - for two-step payments + */ + Status: OperationStatus; + + /** + * Operation type: Payment/Refund/CardPayout + */ + OperationType: OperationType; + + /** + * Order number from payment parameters + */ + InvoiceId?: string; + + /** + * User ID from payment parameters + */ + AccountId?: string; + + /** + * Subscription ID (for recurring payments) + */ + SubscriptionId?: string; + + /** + * Payee token + */ + TokenRecipient?: string; + + /** + * Cardholder name + */ + Name?: string; + + /** + * Payer's e-mail address + */ + Email?: string; + + /** + * Name of the card issuing bank + */ + Issuer: string; + + /** + * ISO3166-1 two-letter country code of the card issuer + */ + IssuerBankCountry?: string; + + /** + * Payment purpose from payment parameters + */ + Description?: string; + + /** + * An arbitrary set of parameters passed to the transaction + */ + Data?: string; +} + +/** + * Pay request body + * https://developers.cloudpayments.ru/#pay + */ +export interface PayRequest extends CardDetails, IpData { + /** + * Number of transaction in the system + */ + TransactionId: number; + + /** + * Payment amount from the payment parameters + */ + Amount: string; + + /** + * Currency: RUB/USD + */ + Currency: Currency; + + /** + * Date/time of payment creation in UTC time zone + */ + DateTime: Date; + + /** + * Test mode sign + */ + TestMode: boolean; + + /** + * Payment status in case of successful completion: + * Completed - for one-step payments, + * Authorized - for two-step payments + */ + Status: OperationStatus; + + /** + * Operation type: Payment/CardPayout + */ + OperationType: Exclude; + + /** + * Acquiring bank identifier + */ + GatewayName: string; + + /** + * Order number from payment parameters + */ + InvoiceId?: string; + + /** + * User ID from payment parameters + */ + AccountId?: string; + + /** + * Subscription ID (for recurring payments) + */ + SubscriptionId?: string; + + /** + * Payee token + */ + TokenRecipient?: string; + + /** + * Cardholder name + */ + Name?: string; + + /** + * Payer's e-mail address + */ + Email?: string; + + /** + * Name of the card issuing bank + */ + Issuer?: string; + + /** + * ISO3166-1 two-letter country code of the card issuer + */ + IssuerBankCountry?: string; + + /** + * Payment purpose from payment parameters + */ + Description?: string; + + /** + * An arbitrary set of parameters passed to the transaction + */ + Data?: string; + + /** + * Total commission value + */ + TotalFee: number; + + /** + * Card product type + */ + CardProduct?: string; + + /** + * Payment method ApplePay or GooglePay + */ + PaymentMethod?: string; + + /** + * First unsuccessful transaction number + */ + FallBackScenarioDeclinedTransactionId?: number; +} + +/** + * Fail request body + * https://developers.cloudpayments.ru/#fail + */ +export interface FailRequest extends CardDetails, IpData { + /** + * Number of transaction in the system + */ + TransactionId: number; + + /** + * Payment amount from the payment parameters + */ + Amount: number; + + /** + * Currency: RUB/USD + */ + Currency: Currency; + + /** + * Date/time of payment creation in UTC time zone + */ + DateTime: Date; + + /** + * Test mode sign + */ + TestMode: boolean; + + /** + * Rejection reason + */ + Reason: string; + + /** + * Error code + * https://developers.cloudpayments.ru/#kody-oshibok + */ + ReasonCode: ReasonCode; + + /** + * Operation type: Payment/Refund/CardPayout + */ + OperationType: OperationType; + + /** + * Order number from payment parameters + */ + InvoiceId?: string; + + /** + * User ID from payment parameters + */ + AccountId?: string; + + /** + * Subscription ID (for recurring payments) + */ + SubscriptionId?: string; + + /** + * Payee token + */ + TokenRecipient?: string; + + /** + * Cardholder name + */ + Name?: string; + + /** + * Payer's e-mail address + */ + Email?: string; + + /** + * Name of the card issuing bank + */ + Issuer: string; + + /** + * ISO3166-1 two-letter country code of the card issuer + */ + IssuerBankCountry?: string; + + /** + * Payment purpose from payment parameters + */ + Description?: string; + + /** + * An arbitrary set of parameters passed to the transaction + */ + Data?: string; + + /** + * Payment method ApplePay or GooglePay + */ + PaymentMethod?: string; + + /** + * First unsuccessful transaction number + */ + FallBackScenarioDeclinedTransactionId?: number; +} + +/** + * Reccurrent request body + * https://developers.cloudpayments.ru/#recurrent + */ +export interface RecurrentRequest { + /** + * Subscription ID + */ + Id: string; + + /** + * User ID + */ + AccountId: string; + + /** + * Free form payment purpose + */ + Description: string; + + /** + * Payer's e-mail + */ + Email: string; + + /** + * Amount of payment + */ + Amount: string; + + /** + * Currency: RUB/USD + */ + Currency: Currency; + + /** + * If the value is true - the payment will be performed according to a two-stage scheme + */ + RequireConfirmation: boolean; + + /** + * Date and time of the first payment according to the plan in the UTC time zone + */ + StartDate: string; + + /** + * Interval. Possible values: Week, Month, Day + */ + Interval: Interval; + + /** + * Period. In combination with the interval, + * 1 Month means once a month, and 2 Week means once every two weeks. + */ + Period: number; + + /** + * Subscription statuses + * https://developers.cloudpayments.ru/#statusy-podpisok-rekurrent + */ + Status: SubscriptionStatus; + + /** + * Number of successful payments + */ + SuccessfulTransactionsNumber: number; + + /** + * The number of unsuccessful payments + * (reset to zero after each successful one) + */ + FailedTransactionsNumber: number; + + /** + * Maximum number of payments in a subscription + */ + MaxPeriods?: number; + + /** + * Date and time of the last successful payment in the UTC time zone + */ + LastTransactionDate?: string; + + /** + * Date and time of the next payment in the UTC time zone + */ + NextTransactionDate?: string; +} + +/** + * Data that we expect in the request + */ +export interface WebhookData extends PaymentData { + /** + * Checksum for validating request and getting data from it + */ + checksum: string; +} diff --git a/src/billing/types/response.ts b/src/billing/types/response.ts new file mode 100644 index 00000000..f5da282c --- /dev/null +++ b/src/billing/types/response.ts @@ -0,0 +1,102 @@ +/** + * Basic response to cloud payments + */ +export interface CPResponse { + /** + * Code of response + */ + code: number; +} + +/** + * Response for the check route + */ +export interface CheckResponse extends CPResponse { + code: CheckCodes; +} + +/** + * Response for the pay route + */ +export interface PayResponse extends CPResponse { + code: PayCodes; +} + +/** + * Response for the fail route + */ +export interface FailResponse extends CPResponse { + code: FailCodes; +} + +/** + * Response for the recurrent route + */ +export interface RecurrentResponse extends CPResponse { + code: RecurrentCodes; +} + +/** + * Codes of check route response + */ +export enum CheckCodes { + /** + * Payment can be made + */ + SUCCESS = 0, + + /** + * Invalid invoice number + */ + INVALID_INVOICE_ID = 10, + + /** + * Incorrect AccountId + */ + INCORRECT_ACCOUNT_ID = 11, + + /** + * Wrong payment amount + */ + WRONG_AMOUNT = 12, + + /** + * Any other reason for refusal + */ + PAYMENT_COULD_NOT_BE_ACCEPTED = 13, + + /** + * Payment is overdue + */ + PAYMENT_IS_OVERDUE = 20 +} + +/** + * Codes of pay route response + */ +export enum PayCodes { + /** + * Payment registered + */ + SUCCESS = 0, +} + +/** + * Codes of fail route response + */ +export enum FailCodes { + /** + * Attempt registered + */ + SUCCESS = 0, +} + +/** + * Codes of reccurrent route response + */ +export enum RecurrentCodes { + /** + * Changes registered + */ + SUCCESS = 0, +} diff --git a/src/directives/defaultValue.ts b/src/directives/defaultValue.ts index 224eac6a..9a623aa5 100644 --- a/src/directives/defaultValue.ts +++ b/src/directives/defaultValue.ts @@ -1,10 +1,10 @@ -import {defaultFieldResolver, GraphQLSchema} from "graphql"; -import {mapSchema, MapperKind, getDirective} from '@graphql-tools/utils' -import {UnknownGraphQLResolverResult} from "../types/graphql"; +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils'; +import { UnknownGraphQLResolverResult } from '../types/graphql'; export default function defaultValueDirective(directiveName = 'default') { return { - defaultValueDirectiveTypeDefs:` + defaultValueDirectiveTypeDefs: ` """ Directive for setting field default value """ @@ -20,6 +20,7 @@ export default function defaultValueDirective(directiveName = 'default') { if (defaultValueDirective) { let { value } = defaultValueDirective as {value: string}; + try { value = JSON.parse(value); } catch (_) { @@ -38,8 +39,9 @@ export default function defaultValueDirective(directiveName = 'default') { return result; }; } + return fieldConfig; - } - }) - } + }, + }), + }; } diff --git a/src/directives/renameFrom.ts b/src/directives/renameFrom.ts index 082f0c2f..0d6a79ab 100644 --- a/src/directives/renameFrom.ts +++ b/src/directives/renameFrom.ts @@ -1,9 +1,9 @@ -import {defaultFieldResolver, GraphQLSchema} from "graphql"; -import {mapSchema, MapperKind, getDirective} from '@graphql-tools/utils' +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils'; export default function renameFromDirective(directiveName = 'renameFrom') { return { - renameFromDirectiveTypeDefs:` + renameFromDirectiveTypeDefs: ` """ Directive for field renaming """ @@ -21,14 +21,16 @@ export default function renameFromDirective(directiveName = 'renameFrom') { const { name } = renameFromDirective as {name: string}; const { resolve = defaultFieldResolver } = fieldConfig; + fieldConfig.resolve = (parent, args, context, info) => { parent[fieldName] = parent[name]; return resolve(parent, args, context, info); - } + }; } + return fieldConfig; - } - }) - } + }, + }), + }; } diff --git a/src/directives/requireAdmin.ts b/src/directives/requireAdmin.ts index a4b1d6d7..453b161b 100644 --- a/src/directives/requireAdmin.ts +++ b/src/directives/requireAdmin.ts @@ -1,8 +1,8 @@ -import {defaultFieldResolver, GraphQLSchema} from "graphql"; -import {mapSchema, MapperKind, getDirective} from '@graphql-tools/utils' -import {ResolverContextWithUser, UnknownGraphQLResolverResult} from "../types/graphql"; -import {ForbiddenError, UserInputError} from "apollo-server-express"; -import WorkspaceModel from "../models/workspace"; +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils'; +import { ResolverContextWithUser, UnknownGraphQLResolverResult } from '../types/graphql'; +import { ForbiddenError, UserInputError } from 'apollo-server-express'; +import WorkspaceModel from '../models/workspace'; /** * Check is user admin via workspace id @@ -87,8 +87,9 @@ export default function requireAdminDirective(directiveName = 'requireAdmin') { return resolve(...resolverArgs); }; } + return fieldConfig; - } - }) - } + }, + }), + }; } diff --git a/src/directives/requireAuth.ts b/src/directives/requireAuth.ts index 313c35b3..d5e903a0 100644 --- a/src/directives/requireAuth.ts +++ b/src/directives/requireAuth.ts @@ -1,8 +1,8 @@ -import {defaultFieldResolver, GraphQLSchema} from "graphql"; -import {mapSchema, MapperKind, getDirective} from '@graphql-tools/utils' -import {ResolverContextBase, UnknownGraphQLResolverResult} from "../types/graphql"; -import {AccessTokenExpiredError} from "../errors"; -import {AuthenticationError} from "apollo-server-express"; +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils'; +import { ResolverContextBase, UnknownGraphQLResolverResult } from '../types/graphql'; +import { AccessTokenExpiredError } from '../errors'; +import { AuthenticationError } from 'apollo-server-express'; /** * Authorizes the user or throws an error if the data is incorrect @@ -20,7 +20,6 @@ function checkUser(context: ResolverContextBase): void { } } - export default function requireAuthDirective(directiveName = 'requireAuth') { return { requireAuthDirectiveTypeDefs: ` @@ -51,8 +50,9 @@ export default function requireAuthDirective(directiveName = 'requireAuth') { return resolve.apply(this, resolverArgs); }; } + return fieldConfig; - } - }) - } + }, + }), + }; } diff --git a/src/directives/requireUserInWorkspace.ts b/src/directives/requireUserInWorkspace.ts index ea344332..092b651b 100644 --- a/src/directives/requireUserInWorkspace.ts +++ b/src/directives/requireUserInWorkspace.ts @@ -1,7 +1,7 @@ -import {defaultFieldResolver, GraphQLSchema} from "graphql"; -import {mapSchema, MapperKind, getDirective} from '@graphql-tools/utils' -import {ResolverContextBase, UnknownGraphQLResolverResult} from "../types/graphql"; -import {ForbiddenError} from "apollo-server-express"; +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils'; +import { ResolverContextBase, UnknownGraphQLResolverResult } from '../types/graphql'; +import { ForbiddenError } from 'apollo-server-express'; /** * Throw error from sync function @@ -59,10 +59,9 @@ async function checkUserInWorkspaceByProjectId(context: ResolverContextBase, pro } } - export default function requireUserInWorkspaceDirective(directiveName = 'requireUserInWorkspace') { return { - requireUserInWorkspaceDirectiveTypeDefs:` + requireUserInWorkspaceDirectiveTypeDefs: ` """ Directive for checking user in workspace """ @@ -97,8 +96,9 @@ export default function requireUserInWorkspaceDirective(directiveName = 'require return resolve.apply(this, resolverArgs); }; } + return fieldConfig; - } - }) - } + }, + }), + }; } diff --git a/src/directives/uploadImageDirective.ts b/src/directives/uploadImageDirective.ts index 6715b4a3..dafd5236 100644 --- a/src/directives/uploadImageDirective.ts +++ b/src/directives/uploadImageDirective.ts @@ -1,7 +1,6 @@ -import {defaultFieldResolver, GraphQLSchema, InputValueDefinitionNode} from "graphql"; -import {mapSchema, MapperKind, getDirective} from '@graphql-tools/utils' -import {BooleanValueNode} from "graphql/language/ast"; -import {save} from "../utils/files"; +import { defaultFieldResolver, GraphQLSchema, InputValueDefinitionNode } from 'graphql'; +import { mapSchema, MapperKind, getDirective } from '@graphql-tools/utils'; +import { save } from '../utils/files'; export default function uploadImageDirective(directiveName = 'uploadImage') { return { @@ -15,26 +14,31 @@ export default function uploadImageDirective(directiveName = 'uploadImage') { mapSchema(schema, { [MapperKind.MUTATION_ROOT_FIELD]: (fieldConfig) => { const fieldArgs = fieldConfig.astNode?.arguments; + if (fieldArgs) { fieldArgs.forEach(arg => { const directives = arg.directives; + directives?.forEach(directive => { if (directive.name.value === directiveName) { - const {resolve = defaultFieldResolver} = fieldConfig; + const { resolve = defaultFieldResolver } = fieldConfig; + fieldConfig.resolve = async (object, args, context, info) => { if (args[arg.name.value]) { const imageMeta = await (args[arg.name.value] as Promise); args[arg.name.value] = await save(imageMeta.file.createReadStream(), imageMeta.mimetype); } + return resolve(object, args, context, info); }; } - }) - }) + }); + }); } + return fieldConfig; - } - }) - } + }, + }), + }; } diff --git a/src/directives/validate.ts b/src/directives/validate.ts index 473a7aad..7dce39ee 100644 --- a/src/directives/validate.ts +++ b/src/directives/validate.ts @@ -1,7 +1,7 @@ -import {defaultFieldResolver, GraphQLSchema} from "graphql"; -import {mapSchema, MapperKind, getDirective, getDirectives} from '@graphql-tools/utils' -import {UserInputError} from "apollo-server-express"; -import {BooleanValueNode} from "graphql/language/ast"; +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { mapSchema, MapperKind, getDirective, getDirectives } from '@graphql-tools/utils'; +import { UserInputError } from 'apollo-server-express'; +import { BooleanValueNode } from 'graphql/language/ast'; /** * Validates string using regex @@ -12,8 +12,8 @@ function checkEmail(email: string): void { const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/g; if (email.match(emailRegex) === null) { - throw new UserInputError('Wrong email format'); -} + throw new UserInputError('Wrong email format'); + } } /** @@ -22,13 +22,21 @@ function checkEmail(email: string): void { */ function checkNotEmpty(str: string): void { if (str.replace(/\s/g, '').length == 0) { - throw new UserInputError('The value must not be empty'); -} + throw new UserInputError('The value must not be empty'); + } } -export default function validateDirective(directiveName = 'validate') { +/** + * + * @param directiveName + * @returns + */ +export default function validateDirective(directiveName = 'validate'): { + validateDirectiveTypeDefs: string; + validateDirectiveTransformer: (schema: GraphQLSchema) => GraphQLSchema; +} { return { - validateDirectiveTypeDefs:` + validateDirectiveTypeDefs: ` """ Directive for checking a field for empty space """ @@ -38,16 +46,20 @@ export default function validateDirective(directiveName = 'validate') { mapSchema(schema, { [MapperKind.MUTATION_ROOT_FIELD]: (fieldConfig, fieldName) => { const args = fieldConfig.astNode?.arguments; + if (args) { args.forEach(arg => { const directives = arg.directives; + directives?.forEach(directive => { if (directive.name.value === directiveName) { const directiveArguments = directive.arguments; const isEmail = (directiveArguments?.find(arg => arg.name.value === 'isEmail')?.value as BooleanValueNode)?.value; const notEmpty = (directiveArguments?.find(arg => arg.name.value === 'notEmpty')?.value as BooleanValueNode)?.value; + if (isEmail || notEmpty) { const { resolve = defaultFieldResolver } = fieldConfig; + fieldConfig.resolve = async (object, args, context, info) => { if (isEmail) { checkEmail(args[arg.name.value] || ''); @@ -55,15 +67,17 @@ export default function validateDirective(directiveName = 'validate') { if (notEmpty) { checkNotEmpty(args[arg.name.value] || ''); } + return resolve(object, args, context, info); }; } } - }) - }) + }); + }); } - return fieldConfig - } - }) - } + + return fieldConfig; + }, + }), + }; } diff --git a/src/index.ts b/src/index.ts index ba8266d7..1ed92aa8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,12 @@ import { GraphQLError } from 'graphql'; import WorkspacesFactory from './models/workspacesFactory'; import DataLoaders from './dataLoaders'; import HawkCatcher from '@hawk.so/nodejs'; +// import { express as voyagerMiddleware } from 'graphql-voyager/middleware'; +/* + * @ts-ignore + * import Accounting from 'codex-accounting-sdk'; + */ +import Billing from './billing'; import bodyParser from 'body-parser'; import { ApolloServerPluginLandingPageGraphQLPlayground, ApolloServerPluginLandingPageDisabled } from 'apollo-server-core'; import ProjectsFactory from './models/projectsFactory'; @@ -19,7 +25,7 @@ import { NonCriticalError } from './errors'; import PlansFactory from './models/plansFactory'; import BusinessOperationsFactory from './models/businessOperationsFactory'; import schema from './schema'; -import {graphqlUploadExpress} from 'graphql-upload' +import { graphqlUploadExpress } from 'graphql-upload'; /** * Option to enable playground @@ -83,6 +89,10 @@ class HawkAPI { next(); }); + const billing = new Billing(); + + billing.appendRoutes(this.app); + this.server = new ApolloServer({ schema, debug: process.env.NODE_ENV === 'development', @@ -176,12 +186,39 @@ class HawkAPI { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const dataLoader = new DataLoaders(mongo.databases.hawk!); + /** + * Initializing accounting SDK + */ + let tlsVerify; + + /** + * Checking env variables + * If at least one path is not transmitted, the variable tlsVerify is undefined + */ + if ( + ![process.env.TLS_CA_CERT, process.env.TLS_CERT, process.env.TLS_KEY].some(value => value === undefined || value.length === 0) + ) { + tlsVerify = { + tlsCaCertPath: `${process.env.TLS_CA_CERT}`, + tlsCertPath: `${process.env.TLS_CERT}`, + tlsKeyPath: `${process.env.TLS_KEY}`, + }; + } + + /* + * const accounting = new Accounting({ + * baseURL: `${process.env.CODEX_ACCOUNTING_URL}`, + * tlsVerify, + * }); + */ + return { factories: HawkAPI.setupFactories(dataLoader), user: { id: userId, accessTokenExpired: isAccessTokenExpired, }, + // accounting, }; } diff --git a/src/models/plan.ts b/src/models/plan.ts index a9a9de06..bdfa7945 100644 --- a/src/models/plan.ts +++ b/src/models/plan.ts @@ -17,10 +17,15 @@ export default class PlanModel extends AbstractModel implements Pl public name!: string; /** - * Monthly charge for plan in dollars + * Monthly charge for plan in currencry specified in `monthlyChargeCurrency` */ public monthlyCharge!: number; + /** + * Currency of `monthlyCharge` + */ + public monthlyChargeCurrency!: string; + /** * Maximum amount of events available for plan */ diff --git a/src/models/usersFactory.ts b/src/models/usersFactory.ts index 75a7ee92..ffa7f249 100644 --- a/src/models/usersFactory.ts +++ b/src/models/usersFactory.ts @@ -82,7 +82,9 @@ export default class UsersFactory extends AbstractModelFactory { - return channel.endpoint.replace(/\s+/, '').trim().length !== 0; + .filter(([_, channel]) => { + return (channel as NotificationsChannelSettingsDBScheme).endpoint.replace(/\s+/, '').trim().length !== 0; }); return notEmptyChannels.length === 0; diff --git a/src/resolvers/user.ts b/src/resolvers/user.ts index e280fbae..bfe663c5 100644 --- a/src/resolvers/user.ts +++ b/src/resolvers/user.ts @@ -10,7 +10,7 @@ import isE2E from '../utils/isE2E'; import { dateFromObjectId } from '../utils/dates'; import { UserDBScheme } from '@hawk.so/types'; import * as telegram from '../utils/telegram'; -import {MongoError} from "mongodb"; +import { MongoError } from 'mongodb'; /** * See all types and fields here {@see ../typeDefs/user.graphql} diff --git a/src/resolvers/workspace.js b/src/resolvers/workspace.js index bcbfad6a..bddb5955 100644 --- a/src/resolvers/workspace.js +++ b/src/resolvers/workspace.js @@ -8,6 +8,7 @@ import { SenderWorkerTaskType } from '../types/userNotifications'; import ProjectToWorkspace from '../models/projectToWorkspace'; import Validator from '../utils/validator'; import { dateFromObjectId } from '../utils/dates'; +import cloudPaymentsApi from '../utils/cloudPaymentsApi'; const { ApolloError, UserInputError, ForbiddenError } = require('apollo-server-express'); const crypto = require('crypto'); @@ -41,11 +42,23 @@ module.exports = { * @param {string} image - workspace image * @param {UserInContext} user - current authorized user {@see ../index.js} * @param {ContextFactories} factories - factories for working with models + * @param {Accounting} accounting - SDK for creating account for new workspace * * @return {WorkspaceModel} created workspace */ - async createWorkspace(_obj, { name, description, image }, { user, factories }) { + async createWorkspace(_obj, { name, description, image }, { user, factories, accounting }) { try { + /* + * Create workspace account and set account id to workspace + * const accountResponse = await accounting.createAccount({ + * name: 'WORKSPACE:' + name, + * type: AccountType.LIABILITY, + * currency: Currency.RUB, + * }); + */ + + // const accountId = accountResponse.recordId; + const accountId = null; /** * @type {WorkspaceDBScheme} @@ -54,7 +67,7 @@ module.exports = { name, description, image, - accountId: '0', + accountId, }; const ownerModel = await factories.usersFactory.findById(user.id); @@ -537,6 +550,8 @@ module.exports = { await cloudPaymentsApi.cancelSubscription(workspaceModel.subscriptionId); + await workspaceModel.setSubscriptionId(null); + return { recordId: workspaceModel._id, record: { diff --git a/src/schema.ts b/src/schema.ts index d2c0320d..faa679bf 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,24 +1,22 @@ import resolvers from './resolvers'; import typeDefs from './typeDefs'; -import {makeExecutableSchema} from "@graphql-tools/schema"; -import renameFromDirective from "./directives/renameFrom"; -import { mergeTypeDefs } from '@graphql-tools/merge' -import defaultValueDirective from "./directives/defaultValue"; -import validateDirective from "./directives/validate"; -import uploadImageDirective from "./directives/uploadImageDirective"; -import requireAuthDirective from "./directives/requireAuth"; -import requireAdminDirective from "./directives/requireAdmin"; -import requireUserInWorkspaceDirective from "./directives/requireUserInWorkspace"; - - -const { renameFromDirectiveTypeDefs, renameFromDirectiveTransformer } = renameFromDirective() -const { defaultValueDirectiveTypeDefs, defaultValueDirectiveTransformer } = defaultValueDirective() -const { validateDirectiveTypeDefs, validateDirectiveTransformer } = validateDirective() -const { uploadImageDirectiveTypeDefs, uploadImageDirectiveTransformer } = uploadImageDirective() -const { requireAuthDirectiveTypeDefs, requireAuthDirectiveTransformer } = requireAuthDirective() -const { requireAdminDirectiveTypeDefs, requireAdminDirectiveTransformer } = requireAdminDirective() -const { requireUserInWorkspaceDirectiveTypeDefs, requireUserInWorkspaceDirectiveTransformer } = requireUserInWorkspaceDirective() +import { makeExecutableSchema } from '@graphql-tools/schema'; +import renameFromDirective from './directives/renameFrom'; +import { mergeTypeDefs } from '@graphql-tools/merge'; +import defaultValueDirective from './directives/defaultValue'; +import validateDirective from './directives/validate'; +import uploadImageDirective from './directives/uploadImageDirective'; +import requireAuthDirective from './directives/requireAuth'; +import requireAdminDirective from './directives/requireAdmin'; +import requireUserInWorkspaceDirective from './directives/requireUserInWorkspace'; +const { renameFromDirectiveTypeDefs, renameFromDirectiveTransformer } = renameFromDirective(); +const { defaultValueDirectiveTypeDefs, defaultValueDirectiveTransformer } = defaultValueDirective(); +const { validateDirectiveTypeDefs, validateDirectiveTransformer } = validateDirective(); +const { uploadImageDirectiveTypeDefs, uploadImageDirectiveTransformer } = uploadImageDirective(); +const { requireAuthDirectiveTypeDefs, requireAuthDirectiveTransformer } = requireAuthDirective(); +const { requireAdminDirectiveTypeDefs, requireAdminDirectiveTransformer } = requireAdminDirective(); +const { requireUserInWorkspaceDirectiveTypeDefs, requireUserInWorkspaceDirectiveTransformer } = requireUserInWorkspaceDirective(); let schema = makeExecutableSchema({ typeDefs: mergeTypeDefs([ @@ -29,10 +27,10 @@ let schema = makeExecutableSchema({ requireAuthDirectiveTypeDefs, requireAdminDirectiveTypeDefs, requireUserInWorkspaceDirectiveTypeDefs, - ...typeDefs + ...typeDefs, ]), resolvers, -}) +}); schema = renameFromDirectiveTransformer(schema); schema = defaultValueDirectiveTransformer(schema); diff --git a/src/typeDefs/billing.ts b/src/typeDefs/billing.ts index 6ebf1abc..807821f3 100644 --- a/src/typeDefs/billing.ts +++ b/src/typeDefs/billing.ts @@ -121,9 +121,14 @@ type PayloadOfWorkspacePlanPurchase { workspace: Workspace! """ - Amount of payment in US cents + 1/100 of the final amount. (US cents for USD, kopecks for RUB) """ amount: Long! + + """ + Currency of payment + """ + currency: String! } """ diff --git a/src/typeDefs/event.ts b/src/typeDefs/event.ts index fff0094e..f84d7105 100644 --- a/src/typeDefs/event.ts +++ b/src/typeDefs/event.ts @@ -394,6 +394,13 @@ type DailyEventInfo { lastRepetitionTime: Float! } +type Subscription { + """ + Sends new events from all user projects + """ + eventOccurred: Event! @requireAuth +} + """ Event information per day with these events """ diff --git a/src/typeDefs/plans.ts b/src/typeDefs/plans.ts index a50eb6cf..7da8191e 100644 --- a/src/typeDefs/plans.ts +++ b/src/typeDefs/plans.ts @@ -20,6 +20,11 @@ export default gql` """ monthlyCharge: Int! + """ + Currency of monthlyCharge + """ + monthlyChargeCurrency: String! + """ Events limit for plan """ diff --git a/src/types/graphql.ts b/src/types/graphql.ts index 772aff43..d3ee4095 100644 --- a/src/types/graphql.ts +++ b/src/types/graphql.ts @@ -2,6 +2,7 @@ import UsersFactory from '../models/usersFactory'; import WorkspacesFactory from '../models/workspacesFactory'; import { GraphQLField } from 'graphql'; import ProjectsFactory from '../models/projectsFactory'; +// import Accounting from 'codex-accounting-sdk'; import PlansFactory from '../models/plansFactory'; import BusinessOperationsFactory from '../models/businessOperationsFactory'; @@ -18,6 +19,11 @@ export interface ResolverContextBase { * Factories for working with models */ factories: ContextFactories; + + // /** + // * SDK for working with CodeX Accounting API + // */ + // accounting: Accounting; } /** diff --git a/src/utils/cloudPaymentsApi.ts b/src/utils/cloudPaymentsApi.ts new file mode 100644 index 00000000..b489586e --- /dev/null +++ b/src/utils/cloudPaymentsApi.ts @@ -0,0 +1,192 @@ +import axios, { AxiosInstance } from 'axios'; + +/** + * Settings for CloudPayments API + */ +interface CloudPaymentsApiSettings { + /** + * Public id for the site + */ + publicId: string; + + /** + * Site's secret + */ + secret: string; +} + +/** + * Data for setting up recurrent payments + */ +interface RecurrentPaymentData { + /** + * Payment interval + */ + interval: 'Day' | 'Week' | 'Month'; + + /** + * Payment period. That is, how often to withdraw money + */ + period: number; + + /** + * Subscription start date (first payment) + */ + startDate?: string; + + /** + * Recurring payment amount. + */ + amount?: number; +} + +/** + * Data for CloudPayments internal purposes + */ +interface CloudPaymentsData { + /** + * Data for recurrent payments + * + * @see https://developers.cloudpayments.ru/#rekurrentnye-platezhi-podpiska + */ + recurrent: RecurrentPaymentData; +} + +/** + * Data to be sent with pay event to and back from payments server + */ +export interface CloudPaymentsJsonData { + /** + * Hash to check data + */ + checksum: string; + + /** + * Data for Cloudpayments needs + */ + cloudPayments?: CloudPaymentsData; +} + +/** + * Payload of the API method to process payment via token + */ +interface PayWithTokenPayload { + /** + * Payment amount + */ + Amount: number; + + /** + * User ID from payment parameters + */ + AccountId: string; + + /** + * Card token for processing payment + */ + Token: string; + + /** + * Other data for request + */ + JsonData: CloudPaymentsJsonData; + + /** + * Currency: RUB/USD + */ + Currency: string; +} + +/** + * Response of the API method to process payment via token + */ +interface PayWithCardResponse { + /** + * Operation status + */ + Success: boolean; + Model: { + /** + * Id of the transaction + */ + TransactionId: number; + }; +} + +/** + * Class for interacting with CloudPayments API + * @see https://developers.cloudpayments.ru/#api + */ +class CloudPaymentsApi { + /** + * CloudPayments public id + */ + private readonly publicId: string; + + /** + * CloudPayments API secret + */ + private readonly secret: string; + + /** + * Axios instance to make calls to API + */ + private readonly api: AxiosInstance; + + /** + * Creates class instance + * @param settings - settings for class initialization + */ + constructor(settings: CloudPaymentsApiSettings) { + this.publicId = settings.publicId; + this.secret = settings.secret; + + this.api = axios.create({ + baseURL: 'https://api.cloudpayments.ru/', + auth: { + username: this.publicId, + password: this.secret, + }, + }); + } + + /** + * Cancel subscription by its id + * + * @param subscriptionId - subscription id to cancel + */ + public async cancelSubscription(subscriptionId: string): Promise { + await this.api.post('/subscriptions/cancel', { + Id: subscriptionId, + }); + } + + /** + * Process payment via token + * + * @param input - data for payment processing + */ + public async payByToken(input: PayWithTokenPayload): Promise { + return (await this.api.post('/payments/tokens/charge', input)).data; + } + + /** + * Cancels the payment by transaction ID + * + * @param transactionId - transaction id to cancel + */ + public async cancelPayment(transactionId: number): Promise { + const result = await this.api.post('/payments/void', { + TransactionId: transactionId, + }); + + if (!result.data.Success) { + throw new Error(`Error during cancelling transaction: ${result.data.Message}`); + } + } +} + +export default new CloudPaymentsApi({ + publicId: process.env.CLOUDPAYMENTS_PUBLIC_ID || '', + secret: process.env.CLOUDPAYMENTS_SECRET || '', +}); diff --git a/test/integration/README.md b/test/integration/README.md new file mode 100644 index 00000000..90e68261 --- /dev/null +++ b/test/integration/README.md @@ -0,0 +1,13 @@ +# Hawk API integration tests + +This folder contains integration tests for API. +All services and tests are started through docker. + +Run following command from the project root to run the tests: +``` +docker-compose -f docker-compose.test.yml up --exit-code-from tests tests +``` +or via yarn: +```shell +yarn test:integration +``` diff --git a/test/integration/accounting.env b/test/integration/accounting.env new file mode 100644 index 00000000..70e4ed50 --- /dev/null +++ b/test/integration/accounting.env @@ -0,0 +1,22 @@ +# API server port +PORT=3999 + +# Option to enable playground (if true playground will be available at /graphql route) +PLAYGROUND_ENABLE=false + +## Hawk Catcher token from hawk.so +HAWK_CATCHER_TOKEN= + +# MongoDB URL for connecting to accounting database +# for running via mono repository +MONGO_ACCOUNTING_DATABASE_URI=mongodb://mongodb:27017/codex_accounting +# for running as standalone +# MONGO_ACCOUNTING_DATABASE_URI=mongodb://localhost:27017/codex_accounting + +# Accounting Cashbook account identifier +CASHBOOK_ACCOUNT_ID=1358955c-3211-471f-be6a-97cc4b771769 +CASHBOOK_ACCOUNT_NAME="Test cashbook account" + +# Accounting Revenue account identifier +REVENUE_ACCOUNT_ID=fcbae90d-508a-46c0-9bbf-1ee0d6e31987 +REVENUE_ACCOUNT_NAME="Test revenue account" diff --git a/test/integration/api.env b/test/integration/api.env new file mode 100644 index 00000000..69703097 --- /dev/null +++ b/test/integration/api.env @@ -0,0 +1,89 @@ +# API server port +PORT=4000 + +# Hawk API database URL +MONGO_HAWK_DB_URL=mongodb://mongodb:27017/hawk + +# Events database URL +MONGO_EVENTS_DB_URL=mongodb://mongodb:27017/hawk_events + + + +# MongoDB settings +MONGO_RECONNECT_TRIES=60 +MONGO_RECONNECT_INTERVAL=1000 + +# JWT secret for user's refresh token +JWT_SECRET_REFRESH_TOKEN=abacaba + +# JWT secret for user's access token +JWT_SECRET_ACCESS_TOKEN=belarus + +# JWT secret for project's tokens (used in catcher) +JWT_SECRET_PROJECT_TOKEN=qwerty + +# JWT secret for signing tokens for processing billing requests +JWT_SECRET_BILLING_CHECKSUM=checksum_secret + +# Option to enable playground (if true playground will be available at /graphql route) +PLAYGROUND_ENABLE=true + +# SMTP mail settings + +## SMTP server adress +SMTP_HOST=smtp.yandex.ru + +## SMTP server port +SMTP_PORT=465 + +## SMTP server username (for examle, from your Yandex account) +SMTP_USERNAME= + +## SMTP server password (for examle, from your Yandex account) +SMTP_PASSWORD= + +## Sender name +SMTP_SENDER_NAME= + +## Mail from which letters are sent +SMTP_SENDER_ADDRESS= + +# AMQP URL +AMQP_URL=amqp://guest:guest@rabbitmq:5672/ + +# Billing settings +BILLING_DEBUG=true +BILLING_COMPANY_EMAIL="team@hawk.so" + +### Accounting module ### +# Accounting service URL +CODEX_ACCOUNTING_URL=http://accounting:3999/graphql + +# Enable or disable tls verify +TLS_VERIFY=false + +# Files with certs +TLS_CA_CERT=/usr/src/app/src/accounting/ca.pem +TLS_CERT=/usr/src/app/src/accounting/tls/client.pem +TLS_KEY=/usr/src/app/src/accounting/tls/client-key.pem + +## GitHub OAuth app client ID +GITHUB_CLIENT_ID=fakedata + +## GitHub OAuth app clinet secret +GITHUB_CLIENT_SECRET=fakedata + +## Hawk API public url (used in OAuth to redirect to callback, should match OAuth app callback URL) +API_URL=http://127.0.0.1:4000 + +## Garage url +GARAGE_URL=http://127.0.0.1:8080 + +## Garage login url +GARAGE_LOGIN_URL=http://127.0.0.1:8080/login + +## Upload dir +UPLOAD_DIR=uploads + +## Hawk Catcher token from hawk.so +HAWK_CATCHER_TOKEN= diff --git a/test/integration/billingMocks.ts b/test/integration/billingMocks.ts new file mode 100644 index 00000000..04b46279 --- /dev/null +++ b/test/integration/billingMocks.ts @@ -0,0 +1,54 @@ +import { UserDBScheme, UserNotificationType } from '@hawk.so/types'; +import { ObjectId } from 'mongodb'; +import { CheckRequest } from '../../src/billing/types'; +import { CardType, Currency, OperationStatus, OperationType } from '../../src/billing/types/enums'; + +export const user: UserDBScheme = { + _id: new ObjectId(), + notifications: { + whatToReceive: { + [UserNotificationType.IssueAssigning]: true, + [UserNotificationType.SystemMessages]: true, + [UserNotificationType.WeeklyDigest]: true, + }, + channels: { + email: { + isEnabled: true, + endpoint: 'test@hawk.so', + minPeriod: 10, + }, + }, + }, +}; +export const transactionId = 880555; +/** + * Basic check request + */ +export const mainRequest: CheckRequest = { + Amount: '20', + CardExpDate: '06/25', + CardFirstSix: '578946', + CardLastFour: '5367', + CardType: CardType.VISA, + Currency: Currency.USD, + DateTime: new Date(), + OperationType: OperationType.PAYMENT, + Status: OperationStatus.COMPLETED, + TestMode: false, + TransactionId: transactionId, + Issuer: 'Codex Bank', +}; + +/** + * Generates request for payment via subscription + * + * @param accountId - id of the account who makes payment + */ +export function getRequestWithSubscription(accountId: string): CheckRequest { + return { + ...mainRequest, + SubscriptionId: '123', + AccountId: accountId, + Amount: '10', + }; +} diff --git a/test/integration/cases/billing/check.test.ts b/test/integration/cases/billing/check.test.ts new file mode 100644 index 00000000..a700d792 --- /dev/null +++ b/test/integration/cases/billing/check.test.ts @@ -0,0 +1,306 @@ +import { apiInstance } from '../../utils'; +import { CheckCodes, CheckRequest } from '../../../../src/billing/types'; +import { Collection, Db } from 'mongodb'; +import { + BusinessOperationDBScheme, + BusinessOperationStatus, + ConfirmedMemberDBScheme, + PlanDBScheme, + UserDBScheme, + WorkspaceDBScheme +} from '@hawk.so/types'; +import checksumService from '../../../../src/utils/checksumService'; +import { getRequestWithSubscription, mainRequest, transactionId } from '../../billingMocks'; +import type { Global } from '@jest/types'; + +declare var global: Global.Global; + +describe('Check webhook', () => { + let accountsDb: Db; + + let businessOperationsCollection: Collection; + let workspacesCollection: Collection; + let plans: Collection; + let users: Collection; + + let workspace: WorkspaceDBScheme; + let externalUser: UserDBScheme; + let member: UserDBScheme; + let admin: UserDBScheme; + let planToChange: PlanDBScheme; + + beforeAll(async () => { + accountsDb = await global.mongoClient.db('hawk'); + + workspacesCollection = await accountsDb.collection('workspaces'); + users = await accountsDb.collection('users'); + plans = await accountsDb.collection('plans'); + + businessOperationsCollection = await accountsDb.collection('businessOperations'); + }); + + beforeEach(async () => { + const currentPlan = (await plans.insertOne({ + name: 'CurrentTestPlan', + monthlyCharge: 10, + monthlyChargeCurrency: 'USD', + eventsLimit: 1000, + isDefault: false, + })).ops[0]; + + workspace = (await workspacesCollection.insertOne({ + name: 'BillingTest', + accountId: '123', + tariffPlanId: currentPlan._id, + } as WorkspaceDBScheme)).ops[0]; + + externalUser = (await users.insertOne({ + email: 'user@billing.test', + })).ops[0]; + + member = (await users.insertOne({ + email: 'member@billing.test', + })).ops[0]; + + admin = (await users.insertOne({ + email: 'admin@billing.test', + })).ops[0]; + + planToChange = (await plans.insertOne({ + name: 'BasicTest', + monthlyCharge: 20, + monthlyChargeCurrency: 'USD', + eventsLimit: 10000, + isDefault: false, + })).ops[0]; + + const team = await accountsDb.collection(`team:${workspace._id.toString()}`); + + await team.insertOne({ + userId: member._id, + }); + + await team.insertOne({ + userId: admin._id, + isAdmin: true, + }); + }); + + afterEach(async () => { + await accountsDb.dropDatabase(); + }); + + describe('With SubscriptionId field only', () => { + test('Should create business operation for workspace with that SubscriptionId', async () => { + const request = getRequestWithSubscription(admin._id.toString()); + + await workspacesCollection.updateOne( + { _id: workspace._id }, + { $set: { subscriptionId: request.SubscriptionId } } + ); + + const apiResponse = await apiInstance.post('/billing/check', request); + const createdBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(CheckCodes.SUCCESS); + expect(createdBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should prohibit payment if no workspace with provided SubscriptionId was found', async () => { + const request = getRequestWithSubscription(admin._id.toString()); + + const apiResponse = await apiInstance.post('/billing/check', request); + const createdBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED); + expect(createdBusinessOperation).toBe(null); + }); + }); + + describe('With SubscriptionId field and Data field', () => { + test.todo('Should prohibit payment if the workspace already has a subscription'); + }); + + describe('With Data field', () => { + test('Should not accept request without necessary data', async () => { + /** + * Request without Data field + */ + const data: CheckRequest = { + ...mainRequest, + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + + expect(apiResponse.data.code).toBe(CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED); + }); + + test('Should not accept request with a non-existent workspace id', async () => { + /** + * Request with a non-existent workspace id + */ + const data: CheckRequest = { + ...mainRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: '5fe383b0126d28907780641b', + userId: admin._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + + expect(apiResponse.data.code).toBe(CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED); + }); + + test('Should not accept request if user is not a member of the workspace', async () => { + /** + * Request with a user who is not a member of the workspace + */ + const data: CheckRequest = { + ...mainRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: externalUser._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + + expect(apiResponse.data.code).toBe(CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED); + }); + + test('Should not accept request if user is not an admin', async () => { + /** + * Request with a user who is not an admin of the workspace + */ + const data: CheckRequest = { + ...mainRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: member._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + + expect(apiResponse.data.code).toBe(CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED); + }); + + test('Should not accept request with non-existent plan', async () => { + /** + * Request with a non-existent plan id + */ + const data: CheckRequest = { + ...mainRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: admin._id.toString(), + tariffPlanId: '5fe383b0126d28007780641b', + shouldSaveCard: false, + }), + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + + expect(apiResponse.data.code).toBe(CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED); + }); + + test('Should not accept request because amount in request doesn\'t match with plan monthly charge', async () => { + /** + * Request with amount that does not match the cost of the plan + */ + const data: CheckRequest = { + ...mainRequest, + Amount: '20.45', + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: admin._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + + expect(apiResponse.data.code).toBe(CheckCodes.WRONG_AMOUNT); + }); + + test('Should create business operation with pending status', async () => { + /** + * Correct data + */ + const data: CheckRequest = { + ...mainRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: admin._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + const createdBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(CheckCodes.SUCCESS); + expect(createdBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + }); + + test('Should allow request with amount = 1$, in case of deferred payment', async () => { + /** + * Correct data + */ + const data: CheckRequest = { + ...mainRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + workspaceId: workspace._id.toString(), + userId: admin._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + cloudPayments: { + recurrent: { + interval: 'Month', + period: 1, + startDate: new Date().toString(), + amount: 1, + }, + }, + }), + }; + + const apiResponse = await apiInstance.post('/billing/check', data); + const createdBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(CheckCodes.SUCCESS); + expect(createdBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); +}); diff --git a/test/integration/cases/billing/fail.test.ts b/test/integration/cases/billing/fail.test.ts new file mode 100644 index 00000000..02c6618f --- /dev/null +++ b/test/integration/cases/billing/fail.test.ts @@ -0,0 +1,254 @@ +import { apiInstance } from '../../utils'; +import { FailCodes, FailRequest } from '../../../../src/billing/types'; +import { CardType, Currency, OperationType, ReasonCode, ReasonCodesTranscript } from '../../../../src/billing/types/enums'; +import { Collection, ObjectId, Db } from 'mongodb'; +import { BusinessOperationDBScheme, BusinessOperationStatus, PlanDBScheme, BusinessOperationType, UserDBScheme, WorkspaceDBScheme, UserNotificationType, PlanProlongationPayload } from '@hawk.so/types'; +import { WorkerPaths } from '../../../../src/rabbitmq'; +import { PaymentFailedNotificationTask, SenderWorkerTaskType } from '../../../../src/types/personalNotifications'; +import checksumService from '../../../../src/utils/checksumService'; +import type { Global } from '@jest/types'; + +declare var global: Global.Global; + +const transactionId = 909090; + +const user: UserDBScheme = { + _id: new ObjectId(), + notifications: { + whatToReceive: { + [UserNotificationType.IssueAssigning]: true, + [UserNotificationType.SystemMessages]: true, + [UserNotificationType.WeeklyDigest]: true, + }, + channels: { + email: { + isEnabled: true, + endpoint: 'test@hawk.so', + minPeriod: 10, + }, + }, + }, +}; + +const workspace = { + _id: new ObjectId(), + accountId: '123', + balance: 0, + billingPeriodEventsCount: 1000, + lastChargeDate: new Date(2020, 10, 4), + name: 'Test workspace', + tariffPlanId: new ObjectId(), + inviteHash: '123456789', +}; + +const tariffPlan: PlanDBScheme = { + _id: new ObjectId(), + eventsLimit: 10000, + isDefault: true, + monthlyCharge: 100, + monthlyChargeCurrency: 'USD', + name: 'Test plan', +}; + +const planProlongationPayload: PlanProlongationPayload = { + userId: user._id.toString(), + workspaceId: workspace._id.toString(), + tariffPlanId: tariffPlan._id.toString(), + shouldSaveCard: false, +}; + +const validRequest: FailRequest = { + Amount: 100, + CardExpDate: '06/25', + CardFirstSix: '578946', + CardLastFour: '5367', + CardType: CardType.VISA, + Currency: Currency.USD, + DateTime: new Date(), + OperationType: OperationType.PAYMENT, + TestMode: false, + TransactionId: transactionId, + Issuer: 'Codex Bank', + Reason: 'DoNotHonor', + ReasonCode: ReasonCode.DO_NOT_HONOR, +}; + +describe('Fail webhook', () => { + let accountsDb: Db; + let businessOperationsCollection: Collection; + let workspacesCollection: Collection; + let usersCollection: Collection; + + beforeAll(async () => { + accountsDb = await global.mongoClient.db('hawk'); + + businessOperationsCollection = await accountsDb.collection('businessOperations'); + workspacesCollection = accountsDb.collection('workspaces'); + usersCollection = accountsDb.collection('users'); + }); + + beforeEach(async () => { + /** + * Add user who makes payment + */ + await usersCollection.insertOne(user); + + /** + * Add workspace for testing it + */ + await workspacesCollection.insertOne(workspace); + + /** + * Add pending business operation to database (like after /billing/check route) + */ + await businessOperationsCollection.insertOne({ + transactionId: transactionId.toString(), + type: BusinessOperationType.DepositByUser, + status: BusinessOperationStatus.Pending, + dtCreated: new Date(), + payload: { + workspaceId: workspace._id, + amount: 200, + currency: 'USD', + userId: user._id, + cardPan: '5367', + }, + }); + + /** + * Clear rabbitmq queue + */ + await global.rabbitChannel.purgeQueue(WorkerPaths.Email.queue); + }); + + afterEach(async () => { + await accountsDb.dropDatabase(); + }); + + describe('With SubscriptionId only', () => { + const request: FailRequest = { + ...validRequest, + SubscriptionId: '123', + AccountId: user._id.toString(), + }; + + request.TransactionId = transactionId; + + beforeEach(async () => { + await workspacesCollection.updateOne( + { _id: workspace._id }, + { $set: { subscriptionId: request.SubscriptionId } } + ); + }); + + test('Should change business operation status to rejected', async () => { + const apiResponse = await apiInstance.post('/billing/fail', request); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Rejected); + }); + + test('Should add task to sender worker to notify user about payment rejection', async () => { + const apiResponse = await apiInstance.post('/billing/fail', request); + + const message = await global.rabbitChannel.get(WorkerPaths.Email.queue, { + noAck: true, + }); + const expectedLimiterTask: PaymentFailedNotificationTask = { + type: SenderWorkerTaskType.PaymentFailed, + payload: { + endpoint: 'test@hawk.so', + workspaceId: workspace._id.toString(), + reason: ReasonCodesTranscript[validRequest.ReasonCode], + }, + }; + + expect(message).toBeTruthy(); + expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + }); + }); + + describe('With valid request', () => { + test('Should change business operation status to rejected', async () => { + const apiResponse = await apiInstance.post('/billing/fail', { + ...validRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum(planProlongationPayload), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Rejected); + }); + + test('Should add task to sender worker to notify user about payment rejection', async () => { + const apiResponse = await apiInstance.post('/billing/fail', { + ...validRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum(planProlongationPayload), + }), + }); + + const message = await global.rabbitChannel.get(WorkerPaths.Email.queue, { + noAck: true, + }); + const expectedLimiterTask: PaymentFailedNotificationTask = { + type: SenderWorkerTaskType.PaymentFailed, + payload: { + endpoint: 'test@hawk.so', + workspaceId: workspace._id.toString(), + reason: ReasonCodesTranscript[validRequest.ReasonCode], + }, + }; + + expect(message).toBeTruthy(); + expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + }); + }); + + describe('With invalid request', () => { + test('Should not change business operation status if no data provided', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { Data, ...invalidRequest } = validRequest; + const apiResponse = await apiInstance.post('/billing/fail', invalidRequest); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no user id provided', async () => { + const apiResponse = await apiInstance.post('/billing/fail', { + ...validRequest, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: '', + workspaceId: workspace._id.toString(), + tariffPlanId: tariffPlan._id.toString(), + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(FailCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + }); +}); diff --git a/test/integration/cases/billing/pay.test.ts b/test/integration/cases/billing/pay.test.ts new file mode 100644 index 00000000..65695589 --- /dev/null +++ b/test/integration/cases/billing/pay.test.ts @@ -0,0 +1,622 @@ +import { accountingEnv, apiInstance } from '../../utils'; +import { PayCodes, PayRequest } from '../../../../src/billing/types'; +import { CardType, Currency, OperationStatus, OperationType } from '../../../../src/billing/types/enums'; +import { Collection, Db, MongoClient, ObjectId } from 'mongodb'; +import { + BusinessOperationDBScheme, + BusinessOperationStatus, + BusinessOperationType, + PlanDBScheme, + UserDBScheme, + WorkspaceDBScheme +} from '@hawk.so/types'; +import { + PaymentSuccessNotificationPayload, + PaymentSuccessNotificationTask, + SenderWorkerTaskType +} from '../../../../src/types/personalNotifications'; +import { WorkerPaths } from '../../../../src/rabbitmq'; +import checksumService from '../../../../src/utils/checksumService'; +import { getRequestWithSubscription, user } from '../../billingMocks'; +import { CardDetails } from '../../../../src/billing/types/cardDetails'; +import type { Global } from '@jest/types'; + +declare var global: Global.Global; + +const transactionId = 123456; + +const currentPlan: PlanDBScheme = { + _id: new ObjectId(), + eventsLimit: 1000, + isDefault: true, + monthlyCharge: 1000, + monthlyChargeCurrency: 'USD', + name: 'Test plan', +}; + +const cardDetails: Required = { + CardExpDate: '25/03', + CardType: CardType.VISA, + Token: '123123', + CardFirstSix: '545636', + CardLastFour: '4555', +}; + +const workspace = { + _id: new ObjectId(), + accountId: '123', + balance: 0, + billingPeriodEventsCount: 1000, + lastChargeDate: new Date(2020, 10, 4), + name: 'Test workspace', + tariffPlanId: currentPlan._id, + inviteHash: '123456789', +}; + +const workspaceAccount = { + id: workspace.accountId, + name: 'WORKSPACE:' + workspace.name, + type: 'Liability', + currency: 'USD', + dtCreated: Date.now(), +}; + +const planToChange: PlanDBScheme = { + _id: new ObjectId(), + eventsLimit: 10000, + isDefault: true, + monthlyCharge: 100, + monthlyChargeCurrency: 'USD', + name: 'Test plan', +}; + +const cashbookAccount = { + id: accountingEnv.CASHBOOK_ACCOUNT_ID, + name: accountingEnv.CASHBOOK_ACCOUNT_NAME, + type: 'Asset', + currency: 'USD', + dtCreated: Date.now(), +}; + +const revenueAccount = { + id: accountingEnv.REVENUE_ACCOUNT_ID, + name: accountingEnv.REVENUE_ACCOUNT_NAME, + type: 'Revenue', + currency: 'USD', + dtCreated: Date.now(), +}; + +const paymentSuccessPayload: PaymentSuccessNotificationPayload = { + userId: user._id.toString(), + workspaceId: workspace._id.toString(), + tariffPlanId: planToChange._id.toString(), +}; + +/** + * Valid data to send to `pay` webhook + * Initializes later in beforeAll + */ +let validPayRequestData: PayRequest; + +describe('Pay webhook', () => { + let accountsDb: Db; + let accountingDb: Db; + let usersCollection: Collection; + let businessOperationsCollection: Collection; + let workspacesCollection: Collection; + let tariffPlanCollection: Collection; + // let accountingCollection: Collection; + // let transactionsCollection: Collection; + + beforeAll(async () => { + validPayRequestData = { + Amount: '10', + CardExpDate: '06/25', + CardFirstSix: '578946', + CardLastFour: '5367', + CardType: CardType.VISA, + Currency: Currency.RUB, + DateTime: new Date(), + GatewayName: 'CodeX bank', + OperationType: OperationType.PAYMENT, + Status: OperationStatus.COMPLETED, + TestMode: false, + TotalFee: 0, + TransactionId: transactionId, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + ...paymentSuccessPayload, + shouldSaveCard: false, + }), + }), + }; + + accountsDb = await global.mongoClient.db('hawk'); + accountingDb = await global.mongoClient.db('codex_accounting'); + + usersCollection = accountsDb.collection('users'); + businessOperationsCollection = accountsDb.collection('businessOperations'); + workspacesCollection = accountsDb.collection('workspaces'); + tariffPlanCollection = accountsDb.collection('plans'); + + // transactionsCollection = accountingDb.collection('transactions'); + // accountingCollection = accountingDb.collection('accounts'); + }); + + beforeEach(async () => { + /** + * Add user who makes payment + */ + await usersCollection.insertOne(user); + + /** + * Add pending business operation to database (like after /billing/check route) + */ + await businessOperationsCollection.insertOne({ + transactionId: transactionId.toString(), + type: BusinessOperationType.DepositByUser, + status: BusinessOperationStatus.Pending, + dtCreated: new Date(), + payload: { + workspaceId: new ObjectId(), + amount: 10, + currency: 'USD', + userId: user._id, + cardPan: '2456', + }, + }); + + /** + * Add workspace for testing it + */ + await workspacesCollection.insertOne(workspace); + + /** + * Add tariff plans for testing + */ + await tariffPlanCollection.insertOne(currentPlan); + await tariffPlanCollection.insertOne(planToChange); + + /** + * Add necessary accounts to accounting system + */ + // await accountingCollection.insertMany([cashbookAccount, revenueAccount, workspaceAccount]); + }); + + afterEach(async () => { + await accountsDb.dropDatabase(); + await accountingDb.dropDatabase(); + await global.rabbitChannel.purgeQueue(WorkerPaths.Limiter.queue); + await global.rabbitChannel.purgeQueue(WorkerPaths.Email.queue); + }); + + describe('With SubscriptionId field only', () => { + const request = getRequestWithSubscription(user._id.toString()); + + request.TransactionId = transactionId; + + beforeEach(async () => { + await workspacesCollection.updateOne( + { _id: workspace._id }, + { $set: { subscriptionId: request.SubscriptionId } } + ); + }); + + test('Should change business operation status to confirmed', async () => { + const apiResponse = await apiInstance.post('/billing/pay', request); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Confirmed); + }); + + test('Should reset events counter in workspace', async () => { + const apiResponse = await apiInstance.post('/billing/pay', request); + + const updatedWorkspace = await workspacesCollection.findOne({ + _id: workspace._id, + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedWorkspace?.billingPeriodEventsCount).toBe(0); + }); + + test('Should reset last charge date in workspace', async () => { + const apiResponse = await apiInstance.post('/billing/pay', request); + + const updatedWorkspace = await workspacesCollection.findOne({ + _id: workspace._id, + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedWorkspace?.lastChargeDate).not.toBe(workspace.lastChargeDate); + }); + + test('Should send task to limiter worker to check workspace', async () => { + const apiResponse = await apiInstance.post('/billing/pay', request); + + const message = await global.rabbitChannel.get('cron-tasks/limiter', { + noAck: true, + }); + const expectedLimiterTask = { + type: 'check-single-workspace', + workspaceId: workspace._id.toString(), + }; + + expect(message).toBeTruthy(); + expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + }); + + // test('Should associate an account with a workspace if the workspace did not have one', async () => { + // /** + // * Remove accountId from existed workspace + // */ + // await workspacesCollection.updateOne( + // { _id: workspace._id }, + // { + // $unset: { + // accountId: '', + // }, + // } + // ); + + // const apiResponse = await apiInstance.post('/billing/pay', request); + + // /** + // * Check that account is created and linked + // */ + // const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + // const accountId = updatedWorkspace?.accountId; + // const account = await accountingCollection.findOne({ id: accountId }); + + // expect(typeof accountId).toBe('string'); + // expect(account).toBeTruthy(); + // expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + // }); + + // test('Should add payment data to accounting system', async () => { + // const apiResponse = await apiInstance.post('/billing/pay', request); + + // const transactions = await transactionsCollection + // .find({}) + // .toArray(); + + // expect(transactions.length).toBe(2); + // expect(transactions.some(tr => tr.type === 'Deposit')); + // expect(transactions.some(tr => tr.type === 'Purchase')); + // expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + // }); + + test('Should add task to sender worker to notify user about successful payment', async () => { + const apiResponse = await apiInstance.post('/billing/pay', request); + + const message = await global.rabbitChannel.get(WorkerPaths.Email.queue, { + noAck: true, + }); + const expectedLimiterTask: PaymentSuccessNotificationTask = { + type: SenderWorkerTaskType.PaymentSuccess, + payload: { + endpoint: 'test@hawk.so', + userId: user._id.toString(), + workspaceId: workspace._id.toString(), + tariffPlanId: currentPlan._id.toString(), + }, + }; + + expect(message).toBeTruthy(); + expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + }); + }); + + describe('With valid request', () => { + test('Should change business operation status to confirmed', async () => { + const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Confirmed); + }); + + test('Should reset events counter in workspace', async () => { + const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + const updatedWorkspace = await workspacesCollection.findOne({ + _id: workspace._id, + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedWorkspace?.billingPeriodEventsCount).toBe(0); + }); + + test('Should reset last charge date in workspace', async () => { + const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + const updatedWorkspace = await workspacesCollection.findOne({ + _id: workspace._id, + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedWorkspace?.lastChargeDate).not.toBe(workspace.lastChargeDate); + }); + + test('Should change workspace plan', async () => { + const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + const updatedWorkspace = await workspacesCollection.findOne({ + _id: workspace._id, + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedWorkspace?.tariffPlanId.toString()).toBe(planToChange._id.toString()); + }); + + test('Should send task to limiter worker to check workspace', async () => { + const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + const message = await global.rabbitChannel.get('cron-tasks/limiter', { + noAck: true, + }); + const expectedLimiterTask = { + type: 'check-single-workspace', + workspaceId: workspace._id.toString(), + }; + + expect(message).toBeTruthy(); + expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + }); + + // test('Should associate an account with a workspace if the workspace did not have one', async () => { + // /** + // * Remove accountId from existed workspace + // */ + // await workspacesCollection.updateOne( + // { _id: workspace._id }, + // { + // $unset: { + // accountId: '', + // }, + // } + // ); + + // const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + // /** + // * Check that account is created and linked + // */ + // const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + // const accountId = updatedWorkspace?.accountId; + // const account = await accountingCollection.findOne({ id: accountId }); + + // expect(typeof accountId).toBe('string'); + // expect(account).toBeTruthy(); + // expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + // }); + + // test('Should add payment data to accounting system', async () => { + // const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + // const transactions = await transactionsCollection + // .find({}) + // .toArray(); + + // expect(transactions.length).toBe(2); + // expect(transactions.some(tr => tr.type === 'Deposit')); + // expect(transactions.some(tr => tr.type === 'Purchase')); + // expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + // }); + + test('Should add task to sender worker to notify user about successful payment', async () => { + const apiResponse = await apiInstance.post('/billing/pay', validPayRequestData); + + const message = await global.rabbitChannel.get(WorkerPaths.Email.queue, { + noAck: true, + }); + const expectedLimiterTask: PaymentSuccessNotificationTask = { + type: SenderWorkerTaskType.PaymentSuccess, + payload: { + endpoint: 'test@hawk.so', + ...paymentSuccessPayload, + }, + }; + + expect(message).toBeTruthy(); + expect(message && JSON.parse(message.content.toString())).toStrictEqual(expectedLimiterTask); + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + }); + + test('Should save SubscriptionId if it is provided in request', async () => { + const request = { + ...validPayRequestData, + SubscriptionId: '123', + }; + + const apiResponse = await apiInstance.post('/billing/pay', request); + + const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + + expect(updatedWorkspace?.subscriptionId).toBe(request.SubscriptionId); + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + }); + + test('Should save user card if shouldSaveCard true', async () => { + /** + * Correct data + */ + const request: PayRequest = { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + ...paymentSuccessPayload, + shouldSaveCard: true, + }), + }), + ...cardDetails, + }; + + const apiResponse = await apiInstance.post('/billing/pay', request); + const updatedUser = await usersCollection.findOne({ _id: user._id }); + + const expectedCard = { + cardExpDate: cardDetails.CardExpDate, + firstSix: +cardDetails.CardFirstSix, + lastFour: +cardDetails.CardLastFour, + token: cardDetails.Token, + type: cardDetails.CardType, + }; + + expect(updatedUser?.bankCards?.length).toBe(1); + expect(updatedUser?.bankCards?.shift()).toMatchObject(expectedCard); + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + }); + }); + + describe('With invalid request', () => { + test('Should not change business operation status if no data or subscription id provided', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { Data, ...invalidRequest } = validPayRequestData; + const apiResponse = await apiInstance.post('/billing/pay', invalidRequest); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no workspace id provided', async () => { + const apiResponse = await apiInstance.post('/billing/pay', { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: user._id.toString(), + workspaceId: '', + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no user id provided', async () => { + const apiResponse = await apiInstance.post('/billing/pay', { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: '', + workspaceId: workspace._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no tariff plan id provided', async () => { + const apiResponse = await apiInstance.post('/billing/pay', { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: user._id.toString(), + workspaceId: workspace._id.toString(), + tariffPlanId: '', + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no workspaces with provided id', async () => { + const apiResponse = await apiInstance.post('/billing/pay', { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: user._id.toString(), + workspaceId: new ObjectId().toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no tariff plan with provided id', async () => { + const apiResponse = await apiInstance.post('/billing/pay', { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: new ObjectId().toString(), + workspaceId: workspace._id.toString(), + tariffPlanId: new ObjectId().toString(), + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + + test('Should not change business operation status if no user with provided id', async () => { + const apiResponse = await apiInstance.post('/billing/pay', { + ...validPayRequestData, + Data: JSON.stringify({ + checksum: await checksumService.generateChecksum({ + userId: new ObjectId().toString(), + workspaceId: workspace._id.toString(), + tariffPlanId: planToChange._id.toString(), + shouldSaveCard: false, + }), + }), + }); + + const updatedBusinessOperation = await businessOperationsCollection.findOne({ + transactionId: transactionId.toString(), + }); + + expect(apiResponse.data.code).toBe(PayCodes.SUCCESS); + expect(updatedBusinessOperation?.status).toBe(BusinessOperationStatus.Pending); + }); + }); +}); diff --git a/test/integration/cases/billing/recurrent.test.ts b/test/integration/cases/billing/recurrent.test.ts new file mode 100644 index 00000000..ec795175 --- /dev/null +++ b/test/integration/cases/billing/recurrent.test.ts @@ -0,0 +1,94 @@ +import { Collection, Db, ObjectId } from 'mongodb'; +import { PlanDBScheme, WorkspaceDBScheme } from '@hawk.so/types'; +import { RecurrentCodes, RecurrentRequest } from '../../../../src/billing/types'; +import { Currency, Interval, SubscriptionStatus } from '../../../../src/billing/types/enums'; +import { apiInstance } from '../../utils'; +import type { Global } from '@jest/types'; + +declare var global: Global.Global; + +const currentPlan: PlanDBScheme = { + _id: new ObjectId(), + eventsLimit: 1000, + isDefault: true, + monthlyCharge: 1000, + monthlyChargeCurrency: 'USD', + name: 'Test plan', +}; + +const workspace: WorkspaceDBScheme = { + _id: new ObjectId(), + accountId: '123', + balance: 0, + billingPeriodEventsCount: 1000, + lastChargeDate: new Date(2020, 10, 4), + name: 'Test workspace', + tariffPlanId: currentPlan._id, + inviteHash: '12345678', + subscriptionId: '123123', +}; + +const request: RecurrentRequest = { + AccountId: '123', + Amount: '123', + Currency: Currency.USD, + Description: 'Description', + Email: 'test@hawk.so', + FailedTransactionsNumber: 0, + Id: '123123', + Interval: Interval.MONTH, + Period: 0, + RequireConfirmation: false, + StartDate: '', + Status: SubscriptionStatus.CANCELLED, + SuccessfulTransactionsNumber: 0, +}; + +describe('Recurrent webhook', () => { + let accountsDb: Db; + let workspacesCollection: Collection; + + beforeAll(async () => { + accountsDb = await global.mongoClient.db('hawk'); + + workspacesCollection = accountsDb.collection('workspaces'); + }); + + beforeEach(async () => { + /** + * Add workspace for testing it + */ + await workspacesCollection.insertOne(workspace); + }); + + afterEach(async () => { + await accountsDb.dropDatabase(); + }); + + describe('Cancelled status', () => { + test('Should remove subscriptionId field from workspace', async () => { + const apiResponse = await apiInstance.post('/billing/recurrent', request); + + const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + + expect(apiResponse.data.code).toBe(RecurrentCodes.SUCCESS); + expect(updatedWorkspace?.subscriptionId).toBeFalsy(); + }); + test.todo('Should notify user about subscription cancelling'); + }); + + describe('Rejected status', () => { + test('Should remove subscriptionId field from workspace', async () => { + const apiResponse = await apiInstance.post('/billing/recurrent', { + ...request, + Status: SubscriptionStatus.REJECTED, + }); + + const updatedWorkspace = await workspacesCollection.findOne({ _id: workspace._id }); + + expect(apiResponse.data.code).toBe(RecurrentCodes.SUCCESS); + expect(updatedWorkspace?.subscriptionId).toBeFalsy(); + }); + test.todo('Should notify user about subscription cancelling'); + }); +}); diff --git a/test/integration/cases/health.test.ts b/test/integration/cases/health.test.ts new file mode 100644 index 00000000..ba5a1090 --- /dev/null +++ b/test/integration/cases/health.test.ts @@ -0,0 +1,9 @@ +import { apiInstance } from '../utils'; + +describe('Server health', () => { + test('Server is healthy', async () => { + const response = await apiInstance.get('.well-known/apollo/server-health'); + + expect(response.status).toBe(200); + }); +}); diff --git a/test/integration/jest.config.js b/test/integration/jest.config.js new file mode 100644 index 00000000..234886e5 --- /dev/null +++ b/test/integration/jest.config.js @@ -0,0 +1,18 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/en/configuration.html + */ + +module.exports = { + /** + * The test environment that will be used for testing + */ + testEnvironment: './jestEnv.js', + + /** + * TypeScript support + */ + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, +}; diff --git a/test/integration/jestEnv.js b/test/integration/jestEnv.js new file mode 100644 index 00000000..cedfba31 --- /dev/null +++ b/test/integration/jestEnv.js @@ -0,0 +1,52 @@ +const NodeEnvironment = require('jest-environment-node'); +const amqp = require('amqplib'); +const mongodb = require('mongodb'); + +/** + * Custom test environment for defining global connections + */ +class CustomEnvironment extends NodeEnvironment { + /** + * Setup environment + * @return {Promise} + */ + async setup() { + await super.setup(); + const mongoClient = new mongodb.MongoClient('mongodb://mongodb:27017', { useUnifiedTopology: true }); + + await mongoClient.connect(); + this.global.mongoClient = mongoClient; + await mongoClient.db('hawk').dropDatabase(); + // await mongoClient.db('codex_accounting').dropDatabase(); + + this.rabbitMqConnection = await amqp.connect('amqp://guest:guest@rabbitmq:5672/'); + this.global.rabbitChannel = await this.rabbitMqConnection.createChannel(); + await this.global.rabbitChannel.purgeQueue('cron-tasks/limiter'); + } + + /** + * Teardown environment + * @return {Promise} + */ + async teardown() { + try { + if (this.global.mongoClient) { + await this.global.mongoClient.close(); + } + + if (this.global.rabbitChannel) { + await this.global.rabbitChannel.close(); + } + + if (this.rabbitMqConnection) { + await this.rabbitMqConnection.close(); + } + } catch (error) { + console.error('Error during teardown:', error); + } + + await super.teardown(); + } +} + +module.exports = CustomEnvironment; diff --git a/test/integration/mocks.ts b/test/integration/mocks.ts new file mode 100644 index 00000000..04b46279 --- /dev/null +++ b/test/integration/mocks.ts @@ -0,0 +1,54 @@ +import { UserDBScheme, UserNotificationType } from '@hawk.so/types'; +import { ObjectId } from 'mongodb'; +import { CheckRequest } from '../../src/billing/types'; +import { CardType, Currency, OperationStatus, OperationType } from '../../src/billing/types/enums'; + +export const user: UserDBScheme = { + _id: new ObjectId(), + notifications: { + whatToReceive: { + [UserNotificationType.IssueAssigning]: true, + [UserNotificationType.SystemMessages]: true, + [UserNotificationType.WeeklyDigest]: true, + }, + channels: { + email: { + isEnabled: true, + endpoint: 'test@hawk.so', + minPeriod: 10, + }, + }, + }, +}; +export const transactionId = 880555; +/** + * Basic check request + */ +export const mainRequest: CheckRequest = { + Amount: '20', + CardExpDate: '06/25', + CardFirstSix: '578946', + CardLastFour: '5367', + CardType: CardType.VISA, + Currency: Currency.USD, + DateTime: new Date(), + OperationType: OperationType.PAYMENT, + Status: OperationStatus.COMPLETED, + TestMode: false, + TransactionId: transactionId, + Issuer: 'Codex Bank', +}; + +/** + * Generates request for payment via subscription + * + * @param accountId - id of the account who makes payment + */ +export function getRequestWithSubscription(accountId: string): CheckRequest { + return { + ...mainRequest, + SubscriptionId: '123', + AccountId: accountId, + Amount: '10', + }; +} diff --git a/test/integration/rabbit.definitions.json b/test/integration/rabbit.definitions.json new file mode 100644 index 00000000..cb39e8b4 --- /dev/null +++ b/test/integration/rabbit.definitions.json @@ -0,0 +1,452 @@ +{ + "//": { + "rabbit_version": "3.7.8" + }, + "users": [ + { + "name": "guest", + "password_hash": "A+lasMnIk+fu2IA6GRuRBAxyps9F2eoWmMnrpBuuMn2QqcS/", + "hashing_algorithm": "rabbit_password_hashing_sha256", + "tags": "administrator" + } + ], + "vhosts": [ + { + "name": "/" + } + ], + "permissions": [ + { + "user": "guest", + "vhost": "/", + "configure": ".*", + "write": ".*", + "read": ".*" + } + ], + "topic_permissions": [], + "parameters": [], + "//": { + "global_parameters": [ + { + "name": "cluster_name", + "value": "rabbit@6542d9b6d360" + } + ] + }, + "policies": [ + { + "vhost": "/", + "name": "stash", + "pattern": "^errors/", + "apply-to": "queues", + "definition": { + "dead-letter-exchange": "stash" + }, + "priority": 0 + }, + { + "vhost": "/", + "name": "stash-notify", + "pattern": "^notify/", + "apply-to": "queues", + "definition": { + "dead-letter-exchange": "stash" + }, + "priority": 0 + } + ], + "queues": [ + { + "name": "errors/golang", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "errors/nodejs", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "errors/javascript", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "errors/client", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "errors/php", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "errors/python", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "errors/test", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/golang", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/nodejs", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/javascript", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/client", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/php", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/python", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/test", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "stash/notify", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "grouper", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "log", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "release/javascript", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "notifier", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "sender/email", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "sender/slack", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "sender/telegram", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "cron-tasks/archiver", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "cron-tasks/limiter", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + }, + { + "name": "cron-tasks/paymaster", + "vhost": "/", + "durable": true, + "auto_delete": false, + "arguments": {} + } + ], + "exchanges": [ + { + "name": "errors", + "vhost": "/", + "type": "direct", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + }, + { + "name": "stash", + "vhost": "/", + "type": "direct", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + }, + { + "name": "notifier", + "vhost": "/", + "type": "direct", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + }, + { + "name": "cron-tasks", + "vhost": "/", + "type": "direct", + "durable": true, + "auto_delete": false, + "internal": false, + "arguments": {} + } + ], + "bindings": [ + { + "source": "errors", + "vhost": "/", + "destination": "errors/client", + "destination_type": "queue", + "routing_key": "errors/client", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "errors/golang", + "destination_type": "queue", + "routing_key": "errors/golang", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "errors/nodejs", + "destination_type": "queue", + "routing_key": "errors/nodejs", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "errors/javascript", + "destination_type": "queue", + "routing_key": "errors/javascript", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "errors/php", + "destination_type": "queue", + "routing_key": "errors/php", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "errors/python", + "destination_type": "queue", + "routing_key": "errors/python", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "errors/test", + "destination_type": "queue", + "routing_key": "errors/test", + "arguments": {} + }, + { + "source": "errors", + "vhost": "/", + "destination": "release/javascript", + "destination_type": "queue", + "routing_key": "release/javascript", + "arguments": {} + }, + { + "source": "stash", + "vhost": "/", + "destination": "stash/client", + "destination_type": "queue", + "routing_key": "errors/client", + "arguments": {} + }, + { + "source": "stash", + "vhost": "/", + "destination": "stash/golang", + "destination_type": "queue", + "routing_key": "errors/golang", + "arguments": {} + }, + { + "source": "stash", + "vhost": "/", + "destination": "stash/nodejs", + "destination_type": "queue", + "routing_key": "errors/nodejs", + "arguments": {} + }, + { + "source": "stash", + "vhost": "/", + "destination": "stash/php", + "destination_type": "queue", + "routing_key": "errors/php", + "arguments": {} + }, + { + "source": "stash", + "vhost": "/", + "destination": "stash/python", + "destination_type": "queue", + "routing_key": "errors/python", + "arguments": {} + }, + { + "source": "stash", + "vhost": "/", + "destination": "stash/test", + "destination_type": "queue", + "routing_key": "errors/test", + "arguments": {} + }, + { + "source": "notifier", + "vhost": "/", + "destination": "notifier", + "destination_type": "queue", + "routing_key": "notifier", + "arguments": {} + }, + { + "source": "notifier", + "vhost": "/", + "destination": "notifier", + "destination_type": "queue", + "routing_key": "notifier", + "arguments": {} + }, + { + "source": "notifier", + "vhost": "/", + "destination": "sender/email", + "destination_type": "queue", + "routing_key": "sender/email", + "arguments": {} + }, + { + "source": "notifier", + "vhost": "/", + "destination": "sender/slack", + "destination_type": "queue", + "routing_key": "sender/slack", + "arguments": {} + }, + { + "source": "notifier", + "vhost": "/", + "destination": "sender/telegram", + "destination_type": "queue", + "routing_key": "sender/slack", + "arguments": {} + }, + { + "source": "cron-tasks", + "vhost": "/", + "destination": "cron-tasks/archiver", + "destination_type": "queue", + "routing_key": "cron-tasks/archiver", + "arguments": {} + }, + { + "source": "cron-tasks", + "vhost": "/", + "destination": "cron-tasks/limiter", + "destination_type": "queue", + "routing_key": "cron-tasks/limiter", + "arguments": {} + }, + { + "source": "cron-tasks", + "vhost": "/", + "destination": "cron-tasks/paymaster", + "destination_type": "queue", + "routing_key": "cron-tasks/paymaster", + "arguments": {} + } + ] +} diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json new file mode 100644 index 00000000..5a2b85a0 --- /dev/null +++ b/test/integration/tsconfig.json @@ -0,0 +1,67 @@ +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + + } +} diff --git a/test/integration/types/global.d.ts b/test/integration/types/global.d.ts new file mode 100644 index 00000000..49a3eaa8 --- /dev/null +++ b/test/integration/types/global.d.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { MongoClient } from 'mongodb'; +import { Channel } from 'amqplib'; + +/** + * Defines global variables for using in tests + */ +declare global { + namespace NodeJS { + interface Global { + /** + * MongoDB client instance + */ + mongoClient: MongoClient; + + /** + * RabbitMQ client instance + */ + rabbitChannel: Channel; + } + } +} diff --git a/test/integration/utils.ts b/test/integration/utils.ts new file mode 100644 index 00000000..26701341 --- /dev/null +++ b/test/integration/utils.ts @@ -0,0 +1,20 @@ +import dotenv from 'dotenv'; +import axios from 'axios'; +import path from 'path'; + +/** + * Env variables for API + */ +export const apiEnv = dotenv.config({ path: path.join(__dirname, './api.env') }).parsed || {}; + +/** + * Env variables for Accounting + */ +export const accountingEnv = dotenv.config({ path: path.join(__dirname, './accounting.env') }).parsed || {}; + +/** + * Axios instance to send requests to API + */ +export const apiInstance = axios.create({ + baseURL: `http://api:${apiEnv.PORT}`, +}); diff --git a/test/models/businessOperation.test.ts b/test/models/businessOperation.test.ts index 49055655..07d63915 100644 --- a/test/models/businessOperation.test.ts +++ b/test/models/businessOperation.test.ts @@ -19,6 +19,7 @@ describe('Business operation model', () => { const payloadDepositByUser = { workspaceId: new ObjectId('5edd36fbb596d4759beb89f6'), amount: 100, + currency: 'USD', userId: new ObjectId('5eb9034a1ccc4421e2623dc2'), cardPan: '4455', }; @@ -42,6 +43,7 @@ describe('Business operation model', () => { amount: 100, userId: new ObjectId(), tariffPlanId: new ObjectId(), + currency: 'RUB', }; const data: BusinessOperationDBScheme = { diff --git a/test/models/businessOperationsFactory.test.ts b/test/models/businessOperationsFactory.test.ts index 682ba911..29dc5a6f 100644 --- a/test/models/businessOperationsFactory.test.ts +++ b/test/models/businessOperationsFactory.test.ts @@ -10,6 +10,7 @@ beforeAll(async () => { }); describe('Business operation factory', () => { + it('should create factory instance', () => { const factory = new BusinessOperationsFactory(mongo.databases.hawk as Db, new DataLoaders(mongo.databases.hawk as Db)); @@ -24,6 +25,7 @@ describe('Business operation factory', () => { amount: 100, userId: new ObjectId('5eb9034a1ccc4421e2623dc2'), cardPan: '4455', + currency: 'RUB', }; const data = { @@ -38,6 +40,7 @@ describe('Business operation factory', () => { expect(businessOperation).toMatchObject(data); }); + }); afterAll(async done => { @@ -46,3 +49,4 @@ afterAll(async done => { done(); }); + diff --git a/test/resolvers/encodedJSON.test.ts b/test/resolvers/encodedJSON.test.ts index c12b3d92..092bac82 100644 --- a/test/resolvers/encodedJSON.test.ts +++ b/test/resolvers/encodedJSON.test.ts @@ -43,30 +43,30 @@ describe('GraphQLEncodedJSON', () => { describe('serialize', () => { it('should support serialization from object', async () => { - const { data, errors } = await graphql( + const { data, errors } = await graphql({ schema, - ` + source: ` query { rootValue } `, - FIXTURE - ); + rootValue: FIXTURE + }); expect(data?.rootValue).toEqual(FIXTURE); expect(errors).toBeUndefined(); }); it('should support serialization from string', async () => { - const { data, errors } = await graphql( + const { data, errors } = await graphql({ schema, - ` + source: ` query { rootValue } `, - JSON.stringify(FIXTURE) - ); + rootValue: JSON.stringify(FIXTURE) + }); expect(data?.rootValue).toEqual(FIXTURE); expect(errors).toBeUndefined(); diff --git a/yarn.lock b/yarn.lock index 870b9f92..e05d4c41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -125,7 +125,16 @@ dependencies: xss "^1.0.8" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": +"@babel/code-frame@^7.0.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== @@ -237,10 +246,10 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-validator-identifier@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" - integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== "@babel/helper-validator-option@^7.18.6": version "7.18.6" @@ -257,13 +266,14 @@ "@babel/types" "^7.18.9" "@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" + integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" + "@babel/helper-validator-identifier" "^7.25.9" + chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.9": version "7.18.9" @@ -451,13 +461,20 @@ axios "^0.21.1" stack-trace "^0.0.10" -"@hawk.so/types@^0.1.15", "@hawk.so/types@^0.1.18": +"@hawk.so/types@^0.1.15": version "0.1.18" resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.18.tgz#746537634756825f066182737429d11ea124d5c5" integrity sha512-SvECLGmLb5t90OSpk3n8DCjJsUoyjrq/Z6Ioil80tVkbMXRdGjaHZpn/0w1gBqtgNWBfW2cSbsQPqmyDj1NsqQ== dependencies: "@types/mongodb" "^3.5.34" +"@hawk.so/types@^0.1.21": + version "0.1.21" + resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.21.tgz#8979b1aff670317e5d092aaefcd879729df88adc" + integrity sha512-Jjy1qznAB7SprVtdgWJS9tdGAaK7rTNWEVWG05m6u3Qd3tTXkgty4D0oAmhTFBnJ/MElPVuundYnDB1r4dHZQQ== + dependencies: + "@types/mongodb" "^3.5.34" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -769,7 +786,16 @@ "@protobufjs/utf8@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + +"@shelf/jest-mongodb@^1.2.2": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@shelf/jest-mongodb/-/jest-mongodb-1.2.3.tgz#7cb34f0bcb71871b0d1c8d16a4f1fdb18b1620df" + integrity sha512-RGECov7b9anpHqrEoegYeZFWN3WEOw/3hPu3fQUi4gnNIGH0jyMVCQd4DgB37n2aoEWFfe7Kq59aQUrgIQRITA== + dependencies: + debug "4.1.1" + mongodb-memory-server "6.6.7" + uuid "8.3.0" "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -912,6 +938,13 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/cross-spawn@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7" + integrity sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw== + dependencies: + "@types/node" "*" + "@types/debug@^4.1.5": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" @@ -919,6 +952,11 @@ dependencies: "@types/ms" "*" +"@types/dedent@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050" + integrity sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A== + "@types/escape-html@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c" @@ -957,6 +995,18 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/find-cache-dir@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/find-cache-dir/-/find-cache-dir-3.2.0.tgz#eaaf331699dccf52c47926e4d4f8f3ed8db33f3c" + integrity sha512-+JeT9qb2Jwzw72WdjU+TSvD5O1QRPWCeRpDJV+guiIq+2hwR0DFGw+nZNbTFjMIVe6Bf4GgAKeB/6Ytx6+MbeQ== + +"@types/find-package-json@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/find-package-json/-/find-package-json-1.1.1.tgz#c0d296ac74fe3309ed0fe75a9c3edb42a776d30c" + integrity sha512-XMCocYkg6VUpkbOQMKa3M5cgc3MvU/LJKQwd3VUJrWZbLr2ARUggupsCAF8DxjEEIuSO6HlnH+vl+XV4bgVeEQ== + dependencies: + "@types/node" "*" + "@types/fs-capacitor@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e" @@ -1061,11 +1111,21 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lockfile@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/lockfile/-/lockfile-1.0.1.tgz#434a3455e89843312f01976e010c60f1bcbd56f7" + integrity sha512-65WZedEm4AnOsBDdsapJJG42MhROu3n4aSSiu87JXF/pSdlubxZxp3S1yz3kTfkJ2KBPud4CpjoHVAptOm9Zmw== + "@types/long@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== +"@types/md5-file@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.2.tgz#c7241e88f4aa17218c774befb0fc34f33f21fe36" + integrity sha512-8gacRfEqLrmZ6KofpFfxyjsm/LYepeWUWUJGaf5A9W9J5B2/dRZMdkDqFDL6YDa9IweH12IO76jO7mpsK2B3wg== + "@types/mime-types@^2.1.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1" @@ -1076,6 +1136,13 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.0.tgz#e9a9903894405c6a6551f1774df4e64d9804d69c" integrity sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w== +"@types/mkdirp@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.1.tgz#0930b948914a78587de35458b86c907b6e98bbf6" + integrity sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q== + dependencies: + "@types/node" "*" + "@types/mongodb@^3.5.34", "@types/mongodb@^3.6.20": version "3.6.20" resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" @@ -1089,6 +1156,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-fetch@^2.5.10": + version "2.5.10" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" + integrity sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node-fetch@^2.5.4": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" @@ -1107,6 +1182,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== +"@types/node@^14.6.4": + version "14.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.2.tgz#d25295f9e4ca5989a2c610754dc02a9721235eeb" + integrity sha512-jeYJU2kl7hL9U5xuI/BhKPZ4vqGM/OmK6whiFAXVhlstzZhVamWhDSmHyGLIp+RVyuF9/d0dqr2P85aFj4BvJg== + +"@types/node@^15.0.2": + version "15.6.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.1.tgz#32d43390d5c62c5b6ec486a9bc9c59544de39a08" + integrity sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA== + "@types/node@^16.11.46": version "16.11.46" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.46.tgz#26047602eefa47b36759d9ebb1b55ad08ce97a73" @@ -1117,6 +1202,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/object-hash@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-2.1.0.tgz#284353e535481690a72bf748619d77577bd23317" + integrity sha512-RW3VRiuQIMo5PJ4Q1IwBtdLHL/t8ACpzUY40norN9ejE6CUBwKetmSxJnITJ0NlzN/ymF1nvPvlpvegtns7yOg== + "@types/prettier@^2.0.0": version "2.6.4" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.4.tgz#ad899dad022bab6b5a9f0a0fe67c2f7a4a8950ed" @@ -1127,11 +1217,21 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== +"@types/qs@^6.9.6": + version "6.9.6" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1" + integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA== + "@types/range-parser@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/semver@^7.3.3": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" + integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== + "@types/serve-static@*": version "1.15.0" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" @@ -1155,6 +1255,16 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/tmp@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c" + integrity sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ== + +"@types/uuid@^8.0.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -1528,6 +1638,13 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-mutex@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df" + integrity sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA== + dependencies: + tslib "^2.3.1" + async-retry@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" @@ -1571,10 +1688,17 @@ aws-sdk@^2.1174.0: uuid "8.0.0" xml2js "0.4.19" +axios@^0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== + dependencies: + follow-redirects "^1.10.0" + axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + version "0.21.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" + integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== dependencies: follow-redirects "^1.14.0" @@ -1692,6 +1816,15 @@ bl@^2.2.0, bl@^2.2.1: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird@^3.5.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -1787,6 +1920,11 @@ bson@^1.1.4: resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -1811,7 +1949,7 @@ buffer@4.9.2: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.6.0: +buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -1864,7 +2002,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: +camelcase@^6.0.0, camelcase@^6.1.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -1881,7 +2019,7 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -chalk@^2.0.0, chalk@^2.1.0: +chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1979,11 +2117,32 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cloudpayments@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cloudpayments/-/cloudpayments-5.0.4.tgz#00d92ddb6a8d2120d6acc38cb14ab95df3119861" + integrity sha512-HjSFGBxkA20bwZjVummkFp0te2RaBL7KDmGAzSoTXlqXm/cVF3gKa6Lc1NF0cviKA9pSL9lfEVQJib98CX3WCQ== + dependencies: + "@types/node" "^15.0.2" + "@types/node-fetch" "^2.5.10" + "@types/object-hash" "^2.1.0" + "@types/qs" "^6.9.6" + node-fetch "^2.6.0" + object-hash "^2.1.1" + qs "^6.10.1" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== +"codex-accounting-sdk@https://github.com/codex-team/codex-accounting-sdk.git": + version "1.2.5" + resolved "https://github.com/codex-team/codex-accounting-sdk.git#7de756db49aea72eebc7e45d8f9c458c9c2d8dce" + dependencies: + "@types/node" "^14.6.4" + axios "^0.20.0" + typescript "^3.7.5" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -2048,6 +2207,11 @@ commander@^2.20.3: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -2120,7 +2284,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2131,7 +2295,18 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0: +cross-spawn@^6.0.5: + version "6.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" + integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.0, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2188,13 +2363,20 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9, debug@~2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== +debug@4, debug@^4.1.0, debug@^4.1.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" +debug@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2202,6 +2384,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.0.1, debug@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2392,7 +2581,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2669,9 +2858,9 @@ esprima@^4.0.0, esprima@^4.0.1: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.0.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -2867,6 +3056,13 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -2911,6 +3107,20 @@ finalhandler@1.2.0: statuses "2.0.1" unpipe "~1.0.0" +find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-package-json@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/find-package-json/-/find-package-json-1.2.0.tgz#4057d1b943f82d8445fe52dc9cf456f6b8b58083" + integrity sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw== + find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -2945,7 +3155,12 @@ fn-args@5.0.0: resolved "https://registry.yarnpkg.com/fn-args/-/fn-args-5.0.0.tgz#7a18e105c8fb3bf0a51c30389bf16c9ebe740bb3" integrity sha512-CtbfI3oFFc3nbdIoHycrfbrxiGgxXBXXuyOl49h47JawM1mYrqpiRqnH5CB2mBatdXvHHOUO6a+RiAuuvKt0lw== -follow-redirects@^1.14.0, follow-redirects@^1.14.9: +follow-redirects@^1.10.0, follow-redirects@^1.14.0: + version "1.14.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + +follow-redirects@^1.14.9: version "1.15.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== @@ -3002,6 +3217,11 @@ fs-capacitor@^6.2.0: resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-6.2.0.tgz#fa79ac6576629163cb84561995602d8999afb7f5" integrity sha512-nKcE1UduoSKX27NSZlg879LdQc94OtbOsEmKMN2MBNudXREvijRKx2GEBsTMTfws+BrbkJoEuynbGSVRSpauvw== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" @@ -3093,6 +3313,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -3321,9 +3546,9 @@ http-proxy-agent@^4.0.1: debug "4" https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== dependencies: agent-base "6" debug "4" @@ -3394,7 +3619,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4354,6 +4579,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lockfile@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609" + integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA== + dependencies: + signal-exit "^3.0.2" + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -4426,7 +4658,7 @@ lru-cache@^7.10.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.13.1.tgz#267a81fbd0881327c46a81c5922606a2cfe336c4" integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ== -make-dir@^3.0.0, make-dir@^3.1.0: +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -4457,6 +4689,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +md5-file@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-5.0.0.tgz#e519f631feca9c39e7f9ea1780b63c4745012e20" + integrity sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4552,11 +4789,16 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.1.1, minimist@^1.2.0: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass@^3.0.0: version "3.3.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" @@ -4592,6 +4834,78 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" +mongodb-memory-server-core@6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/mongodb-memory-server-core/-/mongodb-memory-server-core-6.10.0.tgz#9239c7941e5b0a225b50494563f0fc528c056690" + integrity sha512-Mil7M4w1231laVi3RYckVnvHANgSIHUICzdIxI5N2JM/i+uKamxkgUXmjWob188jWrWrTqeCI2vNq6KoGzRlxQ== + dependencies: + "@types/tmp" "^0.2.0" + async-mutex "^0.3.0" + camelcase "^6.1.0" + debug "^4.2.0" + find-cache-dir "^3.3.1" + find-package-json "^1.2.0" + get-port "^5.1.1" + https-proxy-agent "^5.0.0" + md5-file "^5.0.0" + mkdirp "^1.0.4" + mongodb "^3.6.9" + semver "^7.3.5" + tar-stream "^2.1.4" + tmp "^0.2.1" + tslib "^2.3.0" + uuid "^8.3.1" + yauzl "^2.10.0" + +mongodb-memory-server-core@6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/mongodb-memory-server-core/-/mongodb-memory-server-core-6.6.7.tgz#f402bb5808f052e20d040cd9ce03a64dde94fc62" + integrity sha512-21g2FpQdgqN3sFsj5lbGje1BhrSRGNHgz6gMAl8bvmdpRpoZErclkImVtjBXNHCNmCc1Dxr+EBvH11KaVE+9iQ== + dependencies: + "@types/cross-spawn" "^6.0.2" + "@types/debug" "^4.1.5" + "@types/dedent" "^0.7.0" + "@types/find-cache-dir" "^3.2.0" + "@types/find-package-json" "^1.1.1" + "@types/lockfile" "^1.0.1" + "@types/md5-file" "^4.0.2" + "@types/mkdirp" "^1.0.1" + "@types/semver" "^7.3.3" + "@types/tmp" "^0.2.0" + "@types/uuid" "^8.0.0" + camelcase "^6.0.0" + cross-spawn "^7.0.3" + debug "^4.1.1" + find-cache-dir "^3.3.1" + find-package-json "^1.2.0" + get-port "^5.1.1" + https-proxy-agent "^5.0.0" + lockfile "^1.0.4" + md5-file "^5.0.0" + mkdirp "^1.0.4" + semver "^7.3.2" + tar-stream "^2.1.3" + tmp "^0.2.1" + uuid "^8.2.0" + yauzl "^2.10.0" + optionalDependencies: + mongodb "^3.5.9" + +mongodb-memory-server@6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/mongodb-memory-server/-/mongodb-memory-server-6.6.7.tgz#fa5f1e8153f4248c7c166f99b5143662699f40d6" + integrity sha512-azRGr5csTAl0MCLR/amPCJrmV5TFwRcVtal56dHrPy1o2T8wZRc3AaJyukob8a/JP38JYa/pQnw1AQH7lFA2Cg== + dependencies: + mongodb-memory-server-core "6.6.7" + +mongodb-memory-server@^6.6.1: + version "6.10.0" + resolved "https://registry.yarnpkg.com/mongodb-memory-server/-/mongodb-memory-server-6.10.0.tgz#3011f12b69bd5cd3610eb51df57555bdab5383cb" + integrity sha512-u/n35Jdbl6CwlOlpFcCkMcVsckJNhKsldI2ImePe4+5e/kKgyktS97K5VBv5wppTVOblAbebFInnIsvSts74nQ== + dependencies: + mongodb-memory-server-core "6.10.0" + tslib "^2.3.0" + mongodb@3.5.9: version "3.5.9" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.9.tgz#799b72be8110b7e71a882bb7ce0d84d05429f772" @@ -4605,6 +4919,32 @@ mongodb@3.5.9: optionalDependencies: saslprep "^1.0.0" +mongodb@^3.5.9: + version "3.6.2" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.2.tgz#1154a4ac107bf1375112d83a29c5cf97704e96b6" + integrity sha512-sSZOb04w3HcnrrXC82NEh/YGCmBuRgR+C1hZgmmv4L6dBz4BkRse6Y8/q/neXer9i95fKUBbFi4KgeceXmbsOA== + dependencies: + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +mongodb@^3.6.9: + version "3.7.4" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.4.tgz#119530d826361c3e12ac409b769796d6977037a4" + integrity sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw== + dependencies: + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" + optional-require "^1.1.8" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + mongodb@^3.7.3: version "3.7.3" resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.3.tgz#b7949cfd0adc4cc7d32d3f2034214d4475f175a5" @@ -4628,7 +4968,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4675,6 +5015,11 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== +node-fetch@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -4799,6 +5144,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -5026,12 +5376,22 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -5046,7 +5406,7 @@ pirates@^4.0.1: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pkg-dir@^4.2.0: +pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -5127,7 +5487,12 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== -punycode@^2.1.0, punycode@^2.1.1: +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== @@ -5139,6 +5504,13 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" +qs@^6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" + integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== + dependencies: + side-channel "^1.0.4" + querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -5228,7 +5600,7 @@ readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -5467,7 +5839,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -5479,11 +5851,21 @@ semver@7.x, semver@^7.3.2, semver@^7.3.5: dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0: +semver@^5.5.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.1.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.1.2: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -5913,6 +6295,28 @@ table@^5.2.3: slice-ansi "^2.1.0" string-width "^3.0.0" +tar-stream@^2.1.3: + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^6.1.11: version "6.1.11" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -5964,6 +6368,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -6125,6 +6536,11 @@ tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.3.0, tslib@^2.3.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@~2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -6179,10 +6595,15 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@^3.7.5: + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== + typescript@^4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== unbox-primitive@^1.0.2: version "1.0.2" @@ -6305,7 +6726,12 @@ uuid@8.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== -uuid@^8.0.0, uuid@^8.3.0, uuid@^8.3.2: +uuid@8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + +uuid@^8.0.0, uuid@^8.2.0, uuid@^8.3.0, uuid@^8.3.1, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -6316,9 +6742,9 @@ v8-compile-cache-lib@^3.0.1: integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== v8-to-istanbul@^7.0.0: version "7.1.2" @@ -6467,9 +6893,9 @@ wide-align@^1.1.2: string-width "^1.0.2 || 2 || 3 || 4" word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wrap-ansi@^6.2.0: version "6.2.0" @@ -6583,6 +7009,14 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"