Skip to content

Commit

Permalink
feat(PAYMENTS-15236): add finance details component
Browse files Browse the repository at this point in the history
  • Loading branch information
simbirromanmakarov committed Aug 11, 2023
1 parent 65895ed commit 85789e3
Show file tree
Hide file tree
Showing 73 changed files with 2,061 additions and 212 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"webpack-cli": "^5.1.1"
},
"dependencies": {
"currency-format": "^1.0.13",
"i18next": "^22.5.0",
"tsyringe": "^4.7.0"
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/country/country-code.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum CountryCode {
Croatia = 'HR',
Ghana = 'GH',
India = 'IN',
}
4 changes: 4 additions & 0 deletions src/core/currency/currency.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Currency {
USD = 'USD',
EUR = 'EUR',
}
1 change: 1 addition & 0 deletions src/core/event-name.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export const enum EventName {
getPaymentStatus = 'getPaymentStatus',
legalComponentPing = 'legalComponentPing',
legalComponentPong = 'legalComponentPong',
financeDetails = 'financeDetails',
nextAction = 'nextAction',
}
15 changes: 15 additions & 0 deletions src/core/finance-details/cart-item.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CartLine } from './cart-line.interface';
import { Price } from './price.interface';

export interface CartItem {
key?: string;
imgSrc?: string;
hasDefaultImg?: boolean;
title: string;
price: Price;
priceBeforeDiscount?: Price;
description?: string | null;
tax?: CartLine | null;
quantity?: number;
isBonus: boolean;
}
10 changes: 10 additions & 0 deletions src/core/finance-details/cart-line.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Price } from './price.interface';

export interface CartLine {
key?: string;
title?: string;
content?: string;
money?: Price;
rate?: number;
isDateLine?: boolean;
}
11 changes: 11 additions & 0 deletions src/core/finance-details/cart-summary.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CartLine } from './cart-line.interface';

export interface CartSummary {
transactionDetails?: CartLine[];
shipping?: CartLine[];
subtotal?: CartLine;
subtotalPayment?: CartLine;
subtotalDetails?: CartLine[];
total: CartLine;
totalDetails?: CartLine[];
}
11 changes: 11 additions & 0 deletions src/core/finance-details/checkout-item.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface CheckoutItem {
quantity: number;
amount: number | null;
amount_before_discount?: number | null;
name: string;
image_url: string;
description: string | null;
currency: string;
is_bonus: boolean;
indirect_tax_rate: number;
}
12 changes: 12 additions & 0 deletions src/core/finance-details/finance-details.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CartItem } from './cart-item.interface';
import { CartSummary } from './cart-summary.interface';
import { XpsFinance } from './xps-finance.interface';
import { XpsPurchase } from './xps-purchase.interface';

export interface FinanceDetails {
purchase: XpsPurchase;
finance: XpsFinance;
cartItems: CartItem[];
cartSummary: CartSummary;
paymentCountry: string;
}
4 changes: 4 additions & 0 deletions src/core/finance-details/price.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Price {
amount: number | null;
currency: string;
}
10 changes: 10 additions & 0 deletions src/core/finance-details/virtual-currency.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface VirtualCurrency {
quantity: number;
amount: number | null;
name: string;
image_url: string;
description: string;
longDescription: string | null;
is_bonus: boolean;
currency: string;
}
32 changes: 32 additions & 0 deletions src/core/finance-details/xps-finance.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export interface XpsFinance {
payment_country: { iso: string };
sub_total: {
amount: number;
currency: string;
payment_amount: number;
payment_currency: string;
};
discount: { amount: number; currency: string };
vat_user?: {
amount: number;
percent: number;
currency: string;
visible: boolean;
};
sales_tax: { amount: number; percent: number; currency: string };
sales_tax_user?: { amount: number; percent: number; currency: string };
fee: { amount: number; currency: string };
total: { amount: number; currency: string };
xsolla_credits?: {
payment_amount: number;
payment_currency: string;
};
vat: {
amount: number;
percent: number;
currency: string;
visible: boolean;
};
user_balance?: { amount: number; currency: string };
grand_total?: { amount: number; currency: string };
}
14 changes: 14 additions & 0 deletions src/core/finance-details/xps-purchase.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CheckoutItem } from './checkout-item.interface';
import { VirtualCurrency } from './virtual-currency.interface';

export interface XpsPurchase {
virtual_currency?: VirtualCurrency[];
checkout_items?: CheckoutItem[];
virtual_items?: VirtualCurrency[];
checkout?: {
amount: number;
currency: string;
description: string;
is_bonus?: boolean;
};
}
14 changes: 14 additions & 0 deletions src/core/guards/finance-details-message.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EventName } from '../../core/event-name.enum';
import { Message } from '../../core/message.interface';
import { FinanceDetails } from '../finance-details/finance-details.interface';
import { isEventMessage } from './event-message.guard';

export const isFinanceDetailsEventMessage = (
messageData: unknown
): messageData is Message<FinanceDetails> => {
if (isEventMessage(messageData)) {
return messageData.name === EventName.financeDetails;
}

return false;
};
12 changes: 12 additions & 0 deletions src/core/pipes/currency/currency-format.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
interface SymbolFormat {
grapheme: string;
template: string;
rtl: boolean;
}

export interface CurrencyFormat {
name: string;
fractionSize: number;
symbol: SymbolFormat;
uniqSymbol: SymbolFormat | null;
}
55 changes: 55 additions & 0 deletions src/core/pipes/currency/currency.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { container } from 'tsyringe';
import { DecimalPipe } from '../decimal/decimal.pipe';
import { CurrencyPipe } from './currency.pipe';

describe('CurrencyPipe', () => {
let pipe: CurrencyPipe;
let decimalPipe: DecimalPipe;

beforeEach(() => {
container.clearInstances();
decimalPipe = {
transform: (value) => value.toString(),
};
pipe = container
.createChildContainer()
.register<DecimalPipe>(DecimalPipe, {
useValue: decimalPipe,
})
.resolve(CurrencyPipe);
});

it('Should return null for no value', () => {
const currency = pipe.transform(null, 'USD');
expect(currency).toBeNull();
});

it('Should return null for no currency', () => {
const currency = pipe.transform(1, '');
expect(currency).toBeNull();
});

it('Should format known currency with configuration', () => {
const amount = 1234567890;
const currency = pipe.transform(amount, 'USD');
expect(currency).toEqual('US$1234567890');
});

it('Should return null for known currency and invalid amount', () => {
spyOn(decimalPipe, 'transform').and.returnValue('');
const currency = pipe.transform(1, 'USD');
expect(currency).toBeNull();
});

it('Should format unknown currency with default template', () => {
const amount = 1234567890;
const currency = pipe.transform(amount, 'AAA');
expect(currency).toEqual('1234567890 AAA');
});

it('Should return null for unknown currency and invalid amount', () => {
spyOn(decimalPipe, 'transform').and.returnValue('');
const currency = pipe.transform(1, 'AAA');
expect(currency).toBeNull();
});
});
75 changes: 75 additions & 0 deletions src/core/pipes/currency/currency.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import currencyFormat from 'currency-format/currency-format.json';
import { singleton } from 'tsyringe';
import { DecimalPipe } from '../decimal/decimal.pipe';
import { PipeTransform } from '../pipe-transform.interface';
import { CurrencyFormat } from './currency-format.interface';

@singleton()
export class CurrencyPipe implements PipeTransform {
public constructor(private readonly decimalPipe: DecimalPipe) {}

public transform(
value: number | string | null,
currencyCode?: string
): string | null {
if (!value || !currencyCode) {
return null;
}

return this.formatCurrency(value, currencyCode);
}

private getCurrencyConfig(currencyCode: string): CurrencyFormat | null {
// @ts-expect-error json don't have string type index
const formatForCurrency = currencyFormat[
currencyCode.toUpperCase()
] as unknown as CurrencyFormat | undefined;

if (!formatForCurrency) {
return null;
}

return formatForCurrency;
}

private formatCurrency(
value: number | string,
currencyCode: string
): string | null {
const currencyConfig = this.getCurrencyConfig(currencyCode);

if (!currencyConfig?.uniqSymbol) {
return this.formatCurrencyWithNoConfig(value, currencyCode);
}

const amount = this.decimalPipe.transform(
value,
1,
currencyConfig.fractionSize,
currencyConfig.fractionSize
);

if (!amount) {
return null;
}

const formattedCurrency = currencyConfig.uniqSymbol.template
.replace('1', amount)
.replace('$', currencyConfig.uniqSymbol.grapheme);

return formattedCurrency;
}

private formatCurrencyWithNoConfig(
value: number | string,
currencyCode: string
): string | null {
const amount = this.decimalPipe.transform(value, 1, 2, 2);

if (!amount) {
return null;
}

return `${amount} ${currencyCode}`;
}
}
35 changes: 35 additions & 0 deletions src/core/pipes/decimal/decimal.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Lang } from '../../../core/i18n/lang.enum';
import { container } from 'tsyringe';
import { DecimalPipe } from './decimal.pipe';

describe('DecimalPipe', () => {
let pipe: DecimalPipe;

beforeEach(() => {
pipe = container.createChildContainer().resolve(DecimalPipe);
});

it('Should return empty string for invalid number', () => {
const value = pipe.transform('no-number');
expect(value).toEqual('');
});

it('Should format string number with default settings', () => {
const value = pipe.transform('1234567890.123');
expect(value).toEqual('1,234,567,890.12');
});

it('Should format string number with custom settings', () => {
const minIntDigits = 2;
const minFracDigits = 1;
const maxFracDigits = 3;
const value = pipe.transform(
'1.234',
minIntDigits,
minFracDigits,
maxFracDigits,
Lang.RU
);
expect(value).toEqual('01,234');
});
});
29 changes: 29 additions & 0 deletions src/core/pipes/decimal/decimal.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import i18next from 'i18next';
import { singleton } from 'tsyringe';
import { Lang } from '../../../core/i18n/lang.enum';
import { PipeTransform } from '../pipe-transform.interface';
import { localeValidator } from './locale.validator';

@singleton()
export class DecimalPipe implements PipeTransform {
public transform(
value: number | string,
minIntegerDigits = 1,
minFracDigits = 2,
maxFracDigits = 2,
locale: Lang | null = null
): string {
value = Number(value);

if (isNaN(value)) {
return '';
}

const formatLocale = localeValidator(locale ?? (i18next.language as Lang));
return new Intl.NumberFormat(formatLocale, {
minimumIntegerDigits: minIntegerDigits,
minimumFractionDigits: minFracDigits,
maximumFractionDigits: maxFracDigits,
}).format(value);
}
}
Loading

0 comments on commit 85789e3

Please sign in to comment.