Merge pull request 'fix(SPG-595) Pemesanan - Penyesuaian flow booking' (#26) from fix/transaction into development
continuous-integration/drone/tag Build is passing Details

Reviewed-on: #26
pull/27/head devel_10.6.10
aswin 2024-07-10 10:38:52 +00:00
commit eb32584205
20 changed files with 972 additions and 523 deletions

12
env/env.development vendored
View File

@ -18,4 +18,14 @@ ELASTIC_APM_ACTIVATE=true
ELASTIC_APM_SERVICE_NAME="Skyworld POS"
ELASTIC_APM_SERVER_URL="http://172.10.10.10:8200"
EXPORT_LIMIT_PARTITION=200
CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *"
MIDTRANS_URL=https://app.sandbox.midtrans.com
MIDTRANS_PRODUCTION=false
MIDTRANS_SERVER_KEY=SB-Mid-server-ITmSD6C0nXfIcmgi4TXm6J7i
MIDTRANS_CLIENT_KEY=SB-Mid-client-VFaU_cPL6kh2DKir
EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/"

31
env/env.production vendored Normal file
View File

@ -0,0 +1,31 @@
PORT="3346"
JWT_SECRET="ftyYM4t4kjuj/0ixvIrS18gpdvBJw42NnW71GrFrEhcn0alQkkH7TQIHU5MFFJ1e"
JWT_EXPIRES="24h"
JWT_REFRESH_EXPIRES="7d"
ENC_KEY="921c83f3b90c92dca4ba9b947f99b4c9"
IV="a671a96159e97a4f"
COUCHDB_CONFIG="http://root:password@172.10.10.2:5970"
DEFAULT_DB_HOST="postgres"
DEFAULT_DB_PORT="5432"
DEFAULT_DB_USER="root"
DEFAULT_DB_PASS="password"
DEFAULT_DB_NAME="pos"
ELASTIC_APM_ACTIVATE=true
ELASTIC_APM_SERVICE_NAME="Skyworld POS"
ELASTIC_APM_SERVER_URL="http://172.10.10.10:8200"
CRON_MIDNIGHT="55 11 * * *"
CRON_EVERY_MINUTE="55 11 * * *"
CRON_EVERY_HOUR="0 * * * *"
MIDTRANS_URL=https://app.midtrans.com
MIDTRANS_PRODUCTION=true
MIDTRANS_SERVER_KEY=Mid-server-6lA4Nnmov2BSOcwVq1sLSOpC
MIDTRANS_CLIENT_KEY=Mid-client-JiPIkIPd_RGooF8U
EXPORT_LIMIT_PARTITION=200
ASSETS="https://asset.sky.eigen.co.id/"

View File

@ -37,6 +37,7 @@
"@nestjs/schedule": "^4.1.0",
"@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2",
"algebra.js": "^0.2.6",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
@ -44,6 +45,8 @@
"elastic-apm-node": "^4.5.4",
"exceljs": "^4.4.0",
"googleapis": "^140.0.0",
"mathjs": "^13.0.2",
"midtrans-client": "^1.3.1",
"moment": "^2.30.1",
"nano": "^10.1.3",
"pg": "^8.11.5",

View File

@ -54,6 +54,7 @@ import { ReportExportModule } from './modules/reports/report-export/report-expor
import { ReportBookmarkModel } from './modules/reports/shared/models/report-bookmark.model';
import { ExportReportHistoryModel } from './modules/reports/shared/models/export-report-history.model';
import { CronModule } from './modules/configuration/cron/cron.module';
import { MidtransModule } from './modules/configuration/midtrans/midtrans.module';
@Module({
imports: [
@ -101,6 +102,7 @@ import { CronModule } from './modules/configuration/cron/cron.module';
CronModule,
GoogleCalendarModule,
LogModule,
MidtransModule,
SessionModule,
// user

View File

@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
const midtransClient = require('midtrans-client');
@Injectable()
export class MidtransService {
constructor(private eventBus: EventBus) {}
get midtransInstance() {
return new midtransClient.Snap({
isProduction: false,
serverKey: process.env.MIDTRANS_SERVER_KEY,
clientKey: process.env.MIDTRANS_CLIENT_KEY,
});
}
async getStatus(orderId: string): Promise<any> {
return await this.midtransInstance.transaction.status(orderId);
}
async create(body): Promise<any> {
return await this.midtransInstance.createTransaction({
transaction_details: {
order_id: '123',
gross_amount: 10000,
},
credit_card: {
secure: true,
},
customer_details: {
first_name: 'budd',
last_name: 'pratama',
email: 'budi.pra@mail.com',
phone: '0822111111',
},
});
}
}

View File

@ -0,0 +1,8 @@
export class MidtransCallbackEvent {
constructor(public readonly data: IEventMidtrans) {}
}
export interface IEventMidtrans {
id: string;
data: any;
}

View File

@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class MidtransDto {
@ApiProperty({
type: String,
example: '123',
})
order_id: string;
}

View File

@ -0,0 +1,36 @@
import { Body, Controller, Get, Injectable, Param, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards';
import { MidtransService } from '../data/services/midtrans.service';
import { EventBus } from '@nestjs/cqrs';
import { MidtransCallbackEvent } from '../domain/entities/midtrans-callback.event';
import { MidtransDto } from './dto/midtrans.dto';
@ApiTags(`midtrans`)
@Controller('v1/midtrans')
@Public()
@Injectable()
export class MidtransController {
constructor(
private dataService: MidtransService,
private eventBus: EventBus,
) {}
@Get(':id/status')
async getStatus(@Param('id') id: string) {
return await this.dataService.getStatus(id);
}
@Post('callback')
async callback(@Body() callback: MidtransDto) {
const data = await this.dataService.getStatus(callback?.order_id);
this.eventBus.publishAll([
new MidtransCallbackEvent({
id: data.order_id,
data: data,
}),
]);
console.log(`midtrans callback for order ${data.order_id}`);
return 'success listen callback';
}
}

View File

@ -0,0 +1,20 @@
import { ConfigModule } from '@nestjs/config';
import { CqrsModule } from '@nestjs/cqrs';
import { MidtransController } from './infrastructure/midtrans.controller';
import { MidtransService } from './data/services/midtrans.service';
import { Module } from '@nestjs/common';
@Module({
imports: [
ConfigModule.forRoot(),
// TypeOrmModule.forFeature(
// [
// ],
// CONNECTION_NAME.DEFAULT,
// ),
CqrsModule,
],
controllers: [MidtransController],
providers: [MidtransService],
})
export class MidtransModule {}

View File

@ -11,7 +11,7 @@ import { STATUS } from 'src/core/strings/constants/base.constants';
@Injectable()
export class BatchConfirmReconciliationManager extends BaseBatchUpdateStatusManager<TransactionEntity> {
validateData(data: TransactionEntity): Promise<void> {
async validateData(data: TransactionEntity): Promise<void> {
const net_profit = data.reconciliation_mdr
? Number(this.data.payment_total) - Number(this.data.reconciliation_mdr)
: null;

View File

@ -19,6 +19,7 @@ import { BatchCancelReconciliationManager } from './domain/usecases/managers/bat
import { BatchConfirmReconciliationManager } from './domain/usecases/managers/batch-confirm-reconciliation.manager';
import { RecapReconciliationManager } from './domain/usecases/managers/recap-reconciliation.manager';
import { RecapPosTransactionHandler } from './domain/usecases/handlers/recap-pos-transaction.handler';
import { SalesPriceFormulaDataService } from '../sales-price-formula/data/services/sales-price-formula-data.service';
@Module({
imports: [

View File

@ -0,0 +1,96 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
import { SalesPriceFormulaDataService } from 'src/modules/transaction/sales-price-formula/data/services/sales-price-formula-data.service';
import { TaxDataService } from 'src/modules/transaction/tax/data/services/tax-data.service';
import { FormulaType } from 'src/modules/transaction/sales-price-formula/constants';
import * as math from 'mathjs';
import { Equation, parse } from 'algebra.js';
import { STATUS } from 'src/core/strings/constants/base.constants';
import { TransactionDataService } from '../../../data/services/transaction-data.service';
import { TransactionModel } from '../../../data/models/transaction.model';
@EventsHandler(TransactionChangeStatusEvent)
export class SettledTransactionHandler
implements IEventHandler<TransactionChangeStatusEvent>
{
constructor(
private formulaService: SalesPriceFormulaDataService,
private taxService: TaxDataService,
private dataService: TransactionDataService,
) {}
async handle(event: TransactionChangeStatusEvent) {
const old_data = event.data.old;
const data = event.data.data;
if (
old_data.status == data.status ||
![STATUS.ACTIVE, STATUS.SETTLED].includes(data.status)
)
return;
const profit_formula = await this.formulaService.getOneByOptions({
where: {
type: FormulaType.PROFIT_SHARE,
},
});
const sales_price = await this.formulaService.getOneByOptions({
where: {
type: FormulaType.SALES_PRICE,
},
});
const taxes = await this.taxService.getManyByOptions({
where: {
status: STATUS.ACTIVE,
},
});
const queryRunner = this.dataService
.getRepository()
.manager.connection.createQueryRunner();
// const profit_share_value = this.calculateFormula(profit_formula.formula_string, taxes, data.payment_total_net_profit ?? 0);
// const sale_price_value = this.calculateFormula(sales_price.formula_string, taxes, data.payment_total_net_profit ?? 0);
Object.assign(data, {
profit_share_formula: profit_formula.formula_string,
sales_price_formula: sales_price.formula_string,
});
await this.dataService.create(queryRunner, TransactionModel, data);
}
calculateFormula(formula, taxes, total) {
const regex = /([a-zA-Z0-9_]+)/g;
const variable = {};
const matches: string[] = formula.match(regex);
const uniqueMatches = new Set(matches);
const keys = Array.from(uniqueMatches).filter((key) => key != 'dpp');
for (const key of keys) {
const keyData = taxes.find((tax) => tax.name == key);
variable[key] = keyData.value / 100;
}
try {
const x1 = math.simplify(formula, variable).toString();
console.log('Formula ', x1);
const dppFormula = parse(x1);
const totalFormula = parse(total.toString());
const equation = new Equation(totalFormula, dppFormula);
console.log(equation.toString());
const result = equation.solveFor('dpp').toString();
console.log(result);
const value = math.evaluate(result);
console.log(value);
return value;
} catch (e) {
console.log(e);
}
}
}

View File

@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager';
import { TransactionEntity } from '../../entities/transaction.entity';
import {
EventTopics,
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { TransactionModel } from '../../../data/models/transaction.model';
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
@Injectable()
export class ActiveTransactionManager extends BaseUpdateStatusManager<TransactionEntity> {
getResult(): string {
return `Success active data ${this.result.invoice_code}`;
}
async validateProcess(): Promise<void> {
return;
}
async beforeProcess(): Promise<void> {
return;
}
async afterProcess(): Promise<void> {
return;
}
get validateRelations(): validateRelations[] {
return [];
}
get entityTarget(): any {
return TransactionModel;
}
get eventTopics(): EventTopics[] {
return [
{
topic: TransactionChangeStatusEvent,
data: this.data,
},
];
}
}

View File

@ -0,0 +1,45 @@
import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager';
import { TransactionEntity } from '../../entities/transaction.entity';
import {
EventTopics,
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { TransactionModel } from '../../../data/models/transaction.model';
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BatchActiveTransactionManager extends BaseBatchUpdateStatusManager<TransactionEntity> {
validateData(data: TransactionEntity): Promise<void> {
return;
}
beforeProcess(): Promise<void> {
return;
}
afterProcess(): Promise<void> {
return;
}
get validateRelations(): validateRelations[] {
return [];
}
get entityTarget(): any {
return TransactionModel;
}
get eventTopics(): EventTopics[] {
return [
{
topic: TransactionChangeStatusEvent,
},
];
}
getResult(): BatchResult {
return this.result;
}
}

View File

@ -16,7 +16,7 @@ import { STATUS } from 'src/core/strings/constants/base.constants';
@Injectable()
export class BatchConfirmTransactionManager extends BaseBatchUpdateStatusManager<TransactionEntity> {
validateData(data: TransactionEntity): Promise<void> {
async validateData(data: TransactionEntity): Promise<void> {
if (data.status != STATUS.DRAFT) {
throw new UnprocessableEntityException({
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,

View File

@ -0,0 +1,45 @@
import { BaseBatchUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-batch-update-status.manager';
import { TransactionEntity } from '../../entities/transaction.entity';
import {
EventTopics,
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { TransactionModel } from '../../../data/models/transaction.model';
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BatchInactiveTransactionManager extends BaseBatchUpdateStatusManager<TransactionEntity> {
validateData(data: TransactionEntity): Promise<void> {
return;
}
beforeProcess(): Promise<void> {
return;
}
afterProcess(): Promise<void> {
return;
}
get validateRelations(): validateRelations[] {
return [];
}
get entityTarget(): any {
return TransactionModel;
}
get eventTopics(): EventTopics[] {
return [
{
topic: TransactionChangeStatusEvent,
},
];
}
getResult(): BatchResult {
return this.result;
}
}

View File

@ -2,7 +2,7 @@ import { STATUS } from 'src/core/strings/constants/base.constants';
import { TransactionType } from 'src/modules/transaction/transaction/constants';
export function mappingTransaction(data) {
let payment_type_bank;
let payment_type_bank: any = null;
const season_period = {
id: data.season_period_id,
holiday_name: data.season_period_name,
@ -12,7 +12,7 @@ export function mappingTransaction(data) {
},
};
if (data.payment_type_method_id) {
if (data.payment_type_method_id || data.payment_type_method_name) {
payment_type_bank = {
id: data.payment_type_method_id,
issuer_name: data.payment_type_method_name,
@ -93,6 +93,7 @@ export function mappingRevertTransaction(data, type) {
Object.assign(data, {
type: type,
payment_total_net_profit: data.payment_total,
customer_category_id: data.customer_category?.id ?? null,
customer_category_name: data.customer_category?.name ?? null,
season_period_id: data.season_period?.id ?? null,

View File

@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { BaseUpdateStatusManager } from 'src/core/modules/domain/usecase/managers/base-update-status.manager';
import { TransactionEntity } from '../../entities/transaction.entity';
import {
EventTopics,
validateRelations,
} from 'src/core/strings/constants/interface.constants';
import { TransactionModel } from '../../../data/models/transaction.model';
import { TransactionChangeStatusEvent } from '../../entities/event/transaction-change-status.event';
@Injectable()
export class InactiveTransactionManager extends BaseUpdateStatusManager<TransactionEntity> {
getResult(): string {
return `Success inactive data ${this.result.invoice_code}`;
}
async validateProcess(): Promise<void> {
return;
}
async beforeProcess(): Promise<void> {
return;
}
async afterProcess(): Promise<void> {
return;
}
get validateRelations(): validateRelations[] {
return [];
}
get entityTarget(): any {
return TransactionModel;
}
get eventTopics(): EventTopics[] {
return [
{
topic: TransactionChangeStatusEvent,
data: this.data,
},
];
}
}

View File

@ -29,6 +29,7 @@ import { TaxDataService } from '../tax/data/services/tax-data.service';
import { SalesPriceFormulaDataService } from '../sales-price-formula/data/services/sales-price-formula-data.service';
import { SalesPriceFormulaModel } from '../sales-price-formula/data/models/sales-price-formula.model';
import { TaxModel } from '../tax/data/models/tax.model';
import { SettledTransactionHandler } from './domain/usecases/handlers/settled-transaction.handler';
@Module({
imports: [
@ -48,6 +49,7 @@ import { TaxModel } from '../tax/data/models/tax.model';
controllers: [TransactionDataController, TransactionReadController],
providers: [
PosTransactionHandler,
SettledTransactionHandler,
IndexTransactionManager,
DetailTransactionManager,

1048
yarn.lock

File diff suppressed because it is too large Load Diff