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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hawk.api",
"version": "1.5.3",
"version": "1.5.4",
"main": "index.ts",
"license": "BUSL-1.1",
"scripts": {
Expand Down Expand Up @@ -42,7 +42,7 @@
"@graphql-tools/schema": "^8.5.1",
"@graphql-tools/utils": "^8.9.0",
"@hawk.so/nodejs": "^3.3.2",
"@hawk.so/types": "^0.5.9",
"@hawk.so/types": "^0.6.3",
"@n1ru4l/json-patch-plus": "^0.2.0",
"@node-saml/node-saml": "^5.0.1",
"@octokit/oauth-methods": "^4.0.0",
Expand Down
66 changes: 60 additions & 6 deletions src/billing/cloudpayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import cloudPaymentsApi from '../utils/cloudPaymentsApi';
import PlanModel from '../models/plan';
import { ClientApi, ClientService, CustomerReceiptItem, ReceiptApi, ReceiptTypes, TaxationSystem } from 'cloudpayments';
import PromoCodeService from '../utils/promoCodeService';

const PENNY_MULTIPLIER = 100;

Expand Down Expand Up @@ -88,7 +89,7 @@
return router;
}

/**

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

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Generates invoice id for payment
*
* @param tariffPlan - tariff plan to generate invoice id
Expand Down Expand Up @@ -141,7 +142,7 @@

let workspace: WorkspaceModel;
let member: ConfirmedMemberDBScheme;
let plan: PlanDBScheme;
let plan: PlanModel;
let planId: string;

const { workspaceId, userId, tariffPlanId } = data;
Expand All @@ -161,11 +162,41 @@

const recurrentPaymentSettings = data.cloudPayments?.recurrent;

if (data.promo && !data.isCardLinkOperation) {
try {
const promoCodeService = new PromoCodeService(context.factories);
const promoPricing = await promoCodeService.getPricingForPromoCodeId(
data.promo.id,
data.userId,
data.workspaceId,
plan
);

if (
promoPricing.benefitType !== data.promo.benefitType ||
promoPricing.finalAmount !== data.promo.finalAmount ||
promoPricing.originalAmount !== data.promo.originalAmount ||
promoPricing.discountAmount !== data.promo.discountAmount
) {
this.sendError(res, CheckCodes.WRONG_AMOUNT, '[Billing / Check] Promo code payment data does not match current promo calculation', body);

return;
}
} catch (e) {
const error = e as Error;

this.sendError(res, CheckCodes.PAYMENT_COULD_NOT_BE_ACCEPTED, `[Billing / Check] Promo code is invalid: ${error.toString()}`, body);

return;
}
}

/**
* 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;
const expectedAmount = data.promo?.finalAmount ?? plan.monthlyCharge;
const isRightAmount = +body.Amount === expectedAmount || (!data.promo?.finalAmount && recurrentPaymentSettings?.startDate);

if (!isRightAmount) {
this.sendError(res, CheckCodes.WRONG_AMOUNT, `[Billing / Check] Amount does not equal to plan monthly charge`, body);
Expand Down Expand Up @@ -205,7 +236,7 @@
telegram.sendMessage(`🤗 [Billing / Check] All checks passed successfully «${workspace.name}»`, TelegramBotURLs.Money)
.catch(e => console.error('Error while sending message to Telegram: ' + e));

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

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

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type

res.json({
code: CheckCodes.SUCCESS,
Expand Down Expand Up @@ -295,6 +326,28 @@
if (subscriptionId) {
await workspace.setSubscriptionId(subscriptionId);
}

if (data.promo && !data.isCardLinkOperation) {
const promoCodeService = new PromoCodeService(req.context.factories);
const promoPricing = await promoCodeService.getPricingForPromoCodeId(
data.promo.id,
data.userId,
data.workspaceId,
tariffPlan
);

await promoCodeService.createUsage({
promoCode: promoPricing.promoCode,
userId: data.userId,
workspaceId: workspace._id,
planId: tariffPlan._id,
benefitType: data.promo.benefitType,
originalAmount: data.promo.originalAmount,
finalAmount: data.promo.finalAmount,
discountAmount: data.promo.discountAmount,
utm: data.promo.utm,
});
}
} catch (e) {
const error = e as Error;

Expand Down Expand Up @@ -442,7 +495,7 @@
*/
const userEmail = body.IssuerBankCountry === RUSSIA_ISO_CODE ? user.email : undefined;

await this.sendReceipt(workspace, tariffPlan, userEmail);
await this.sendReceipt(workspace, tariffPlan, userEmail, data.promo?.finalAmount ?? tariffPlan.monthlyCharge);

let messageText = '';

Expand Down Expand Up @@ -555,7 +608,7 @@

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

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

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

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type

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

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

View workflow job for this annotation

GitHub Actions / ESlint

Unexpected any. Specify a different type
res.json({
code: errorCode,
});
Expand Down Expand Up @@ -802,7 +855,7 @@
promise.catch(e => console.error('Error while sending message to Telegram: ' + e));
}

/**

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

View workflow job for this annotation

GitHub Actions / ESlint

Missing JSDoc @returns for function
* Parses body and returns card data
* @param request - request body to parse
*/
Expand All @@ -826,8 +879,9 @@
* @param workspace - workspace for which payment is made
* @param tariff - paid tariff plan
* @param userMail - user email address
* @param amount - actual paid amount
*/
private async sendReceipt(workspace: WorkspaceModel, tariff: PlanModel, userMail?: string): Promise<void> {
private async sendReceipt(workspace: WorkspaceModel, tariff: PlanModel, userMail?: string, amount = tariff.monthlyCharge): Promise<void> {
/**
* A general tax that applies to all commercial activities
* involving the production and distribution of goods and the provision of services
Expand All @@ -836,9 +890,9 @@
const VALUE_ADDED_TAX = 0;

const item: CustomerReceiptItem = {
amount: tariff.monthlyCharge,
amount,
label: `${tariff.name} tariff plan`,
price: tariff.monthlyCharge,
price: amount,
vat: VALUE_ADDED_TAX,
quantity: 1,
};
Expand Down
41 changes: 41 additions & 0 deletions src/billing/types/paymentData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Utm } from '@hawk.so/types';

/**
* Data for setting up recurring payments
*/
Expand Down Expand Up @@ -35,6 +37,41 @@ interface CloudPaymentsSettings {
recurrent: RecurrentPaymentSettings;
}

/**
* Promo data attached to payment request
*/
export interface PaymentPromoData {
/**
* Applied promo code id
*/
id: string;

/**
* Promo benefit type
*/
benefitType: 'percent_discount' | 'amount_discount' | 'fixed_price';

/**
* Plan price before promo
*/
originalAmount: number;

/**
* Final price after promo
*/
finalAmount: number;

/**
* Actual discount amount
*/
discountAmount: number;

/**
* UTM parameters captured when promo was applied
*/
utm?: Utm;
}

export interface PaymentData {
/**
* Data for Cloudpayments needs
Expand All @@ -56,6 +93,10 @@ export interface PaymentData {
* If true, we will save user card
*/
shouldSaveCard: boolean;
/**
* Applied promo code data
*/
promo?: PaymentPromoData;
/**
* True if this is card linking operation – charging minimal amount of money to validate card info
*/
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import ReleasesFactory from './models/releasesFactory';
import RedisHelper from './redisHelper';
import { appendSsoRoutes } from './sso';
import { appendGitHubRoutes } from './integrations/github';
import PromoCodesFactory from './models/promoCodesFactory';
import PromoCodeUsagesFactory from './models/promoCodeUsagesFactory';

/**
* Option to enable playground
Expand Down Expand Up @@ -172,13 +174,21 @@ class HawkAPI {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const releasesFactory = new ReleasesFactory(mongo.databases.events!);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const promoCodesFactory = new PromoCodesFactory(mongo.databases.hawk!);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const promoCodeUsagesFactory = new PromoCodeUsagesFactory(mongo.databases.hawk!);

return {
usersFactory,
workspacesFactory,
projectsFactory,
plansFactory,
businessOperationsFactory,
releasesFactory,
promoCodesFactory,
promoCodeUsagesFactory,
};
}

Expand Down
66 changes: 66 additions & 0 deletions src/models/promoCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Collection, ObjectId } from 'mongodb';
import AbstractModel from './abstractModel';
import {
PromoCodeBenefit,
PromoCodeDBScheme
} from '@hawk.so/types';

/**
* Model representing promo code settings.
*/
export default class PromoCodeModel extends AbstractModel<PromoCodeDBScheme> implements PromoCodeDBScheme {
/**
* Promo code id.
*/
public _id!: ObjectId;

/**
* Normalized promo code value.
*/
public value!: string;

/**
* Benefit granted by this promo code.
*/
public benefit!: PromoCodeBenefit;

/**
* Maximum successful usages count.
*/
public limit?: number;

/**
* Expiration date.
*/
public expiresAt?: Date;

/**
* Creation date.
*/
public createdAt!: Date;

/**
* Last update date.
*/
public updatedAt!: Date;

/**
* Creator id.
*/
public createdBy!: string;

/**
* Model's collection.
*/
protected collection: Collection<PromoCodeDBScheme>;

/**
* Create PromoCode instance.
*
* @param promoCodeData - promo code data
*/
constructor(promoCodeData: PromoCodeDBScheme) {
super(promoCodeData);
this.collection = this.dbConnection.collection<PromoCodeDBScheme>('promoCodes');
}
}
81 changes: 81 additions & 0 deletions src/models/promoCodeUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Collection, ObjectId } from 'mongodb';
import AbstractModel from './abstractModel';
import {
PromoCodeBenefitType,
PromoCodeUsageDBScheme
} from '@hawk.so/types';

/**
* Model representing successful promo code application.
*/
export default class PromoCodeUsageModel extends AbstractModel<PromoCodeUsageDBScheme> implements PromoCodeUsageDBScheme {
/**
* Promo code usage id.
*/
public _id!: ObjectId;

/**
* Applied promo code id.
*/
public promoCodeId!: ObjectId;

/**
* User who applied promo code.
*/
public userId!: string;

/**
* Workspace where promo code was applied.
*/
public workspaceId!: ObjectId;

/**
* Plan to which promo was applied.
*/
public planId?: ObjectId;

/**
* Benefit type at application time.
*/
public benefitType!: PromoCodeBenefitType;

/**
* Price before promo.
*/
public originalAmount?: number;

/**
* Price after promo.
*/
public finalAmount?: number;

/**
* Actual discount amount.
*/
public discountAmount?: number;

/**
* UTM parameters captured on apply.
*/
public utm?: PromoCodeUsageDBScheme['utm'];

/**
* Application date.
*/
public appliedAt!: Date;

/**
* Model's collection.
*/
protected collection: Collection<PromoCodeUsageDBScheme>;

/**
* Create PromoCodeUsage instance.
*
* @param usageData - usage data
*/
constructor(usageData: PromoCodeUsageDBScheme) {
super(usageData);
this.collection = this.dbConnection.collection<PromoCodeUsageDBScheme>('promoCodeUsages');
}
}
Loading
Loading