Compare commits

..

10 Commits

35 changed files with 1466 additions and 441 deletions
+4 -1
View File
@@ -1,7 +1,10 @@
import { PasswordUtil } from '@/common/utils/password.util' import { PasswordUtil } from '@/common/utils/password.util'
import { GoodPricingModel } from '@/generated/prisma/enums' import { GoodPricingModel } from '@/generated/prisma/enums'
import { GoodCreateManyInput } from '@/generated/prisma/models' import { GoodCreateManyInput } from '@/generated/prisma/models'
import { prisma } from '../src/lib/prisma' import { PrismaClient } from '../src/generated/prisma/client'
import { prismaAdapter } from '../src/lib/prisma'
const prisma = new PrismaClient({ adapter: prismaAdapter })
async function main() { async function main() {
const password = await PasswordUtil.hash('123456') const password = await PasswordUtil.hash('123456')
+57 -58
View File
@@ -1,66 +1,65 @@
#!/usr/bin/env ts-node // #!/usr/bin/env ts-node
import * as fs from 'fs' // import * as fs from 'fs'
import * as path from 'path' // import * as path from 'path'
import { prisma } from '../src/lib/prisma'
function findModules(dir: string): string[] { // function findModules(dir: string): string[] {
const results: string[] = [] // const results: string[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true }) // const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const e of entries) { // for (const e of entries) {
const full = path.join(dir, e.name) // const full = path.join(dir, e.name)
if (e.isDirectory()) { // if (e.isDirectory()) {
results.push(...findModules(full)) // results.push(...findModules(full))
} else if (e.isFile() && e.name.endsWith('.module.ts')) { // } else if (e.isFile() && e.name.endsWith('.module.ts')) {
results.push(full) // results.push(full)
} // }
} // }
return results // return results
}
function moduleNameFromFile(filePath: string) {
const name = path.basename(filePath).replace('.module.ts', '')
return name.replace(/-module$|\.module$/i, '')
}
function makePermissionsFor(moduleName: string) {
const base = moduleName.replace(/\W+/g, '_').toLowerCase()
return [`${base}:create`, `${base}:read`, `${base}:update`, `${base}:delete`]
}
async function main() {
// const srcDir = path.resolve(__dirname, '..', 'src')
// const moduleFiles = findModules(srcDir)
// const modules = moduleFiles.map(moduleNameFromFile)
// const permsMap: Record<string, boolean> = {}
// for (const m of modules) {
// for (const p of makePermissionsFor(m)) permsMap[p] = false
// } // }
// // Upsert roles: ensure admin has full permissions, others get entries added // function moduleNameFromFile(filePath: string) {
// const roles = await prisma.role.findMany() // const name = path.basename(filePath).replace('.module.ts', '')
// for (const r of roles) { // return name.replace(/-module$|\.module$/i, '')
// const current: Record<string, any> = (r.permissions as any) || {}
// const merged = { ...permsMap, ...current }
// // If role name is admin (case-insensitive), set all permissions to true
// if (r.name && r.name.toLowerCase() === 'admin') {
// for (const key of Object.keys(merged)) merged[key] = true
// } else {
// // keep existing truthy values, otherwise false
// for (const key of Object.keys(merged)) merged[key] = merged[key] || false
// } // }
// await prisma.role.update({ where: { id: r.id }, data: { permissions: merged } }) // function makePermissionsFor(moduleName: string) {
// console.log( // const base = moduleName.replace(/\W+/g, '_').toLowerCase()
// `Updated role ${r.name} (id=${r.id}) with ${Object.keys(merged).length} permissions`, // return [`${base}:create`, `${base}:read`, `${base}:update`, `${base}:delete`]
// )
// } // }
await prisma.$disconnect() // async function main() {
} // // const srcDir = path.resolve(__dirname, '..', 'src')
// // const moduleFiles = findModules(srcDir)
// // const modules = moduleFiles.map(moduleNameFromFile)
main().catch(e => { // // const permsMap: Record<string, boolean> = {}
console.error(e) // // for (const m of modules) {
process.exit(1) // // for (const p of makePermissionsFor(m)) permsMap[p] = false
}) // // }
// // // Upsert roles: ensure admin has full permissions, others get entries added
// // const roles = await prisma.role.findMany()
// // for (const r of roles) {
// // const current: Record<string, any> = (r.permissions as any) || {}
// // const merged = { ...permsMap, ...current }
// // // If role name is admin (case-insensitive), set all permissions to true
// // if (r.name && r.name.toLowerCase() === 'admin') {
// // for (const key of Object.keys(merged)) merged[key] = true
// // } else {
// // // keep existing truthy values, otherwise false
// // for (const key of Object.keys(merged)) merged[key] = merged[key] || false
// // }
// // await prisma.role.update({ where: { id: r.id }, data: { permissions: merged } })
// // console.log(
// // `Updated role ${r.name} (id=${r.id}) with ${Object.keys(merged).length} permissions`,
// // )
// // }
// await prisma.$disconnect()
// }
// main().catch(e => {
// console.error(e)
// process.exit(1)
// })
+121 -122
View File
@@ -1,144 +1,143 @@
import fs from 'node:fs' // import fs from 'node:fs'
import path from 'node:path' // import path from 'node:path'
import { prisma } from '../src/lib/prisma'
type CsvRow = { // type CsvRow = {
ID?: string // ID?: string
DescriptionOfID?: string // DescriptionOfID?: string
Vat?: string // Vat?: string
Type?: string // Type?: string
} // }
function parseCsvLine(line: string): string[] { // function parseCsvLine(line: string): string[] {
const result: string[] = [] // const result: string[] = []
let current = '' // let current = ''
let inQuotes = false // let inQuotes = false
for (let index = 0; index < line.length; index++) { // for (let index = 0; index < line.length; index++) {
const char = line[index] // const char = line[index]
if (char === '"') { // if (char === '"') {
if (inQuotes && line[index + 1] === '"') { // if (inQuotes && line[index + 1] === '"') {
current += '"' // current += '"'
index++ // index++
} else { // } else {
inQuotes = !inQuotes // inQuotes = !inQuotes
} // }
continue // continue
} // }
if (char === ',' && !inQuotes) { // if (char === ',' && !inQuotes) {
result.push(current) // result.push(current)
current = '' // current = ''
continue // continue
} // }
current += char // current += char
} // }
result.push(current) // result.push(current)
return result.map(value => value.trim()) // return result.map(value => value.trim())
} // }
function parseCsv(content: string): CsvRow[] { // function parseCsv(content: string): CsvRow[] {
const lines = content // const lines = content
.split(/\r?\n/) // .split(/\r?\n/)
.map(line => line.trim()) // .map(line => line.trim())
.filter(Boolean) // .filter(Boolean)
if (!lines.length) return [] // if (!lines.length) return []
const headers = parseCsvLine(lines[0]) // const headers = parseCsvLine(lines[0])
return lines.slice(1).map(line => { // return lines.slice(1).map(line => {
const cols = parseCsvLine(line) // const cols = parseCsvLine(line)
const row: Record<string, string> = {} // const row: Record<string, string> = {}
for (let index = 0; index < headers.length; index++) { // for (let index = 0; index < headers.length; index++) {
row[headers[index]] = cols[index] || '' // row[headers[index]] = cols[index] || ''
} // }
return row // return row
}) // })
} // }
function toBooleanFlags(typeValue: string) { // function toBooleanFlags(typeValue: string) {
const normalized = typeValue || '' // const normalized = typeValue || ''
const isPublic = normalized.includes('شناسه عمومی') // const isPublic = normalized.includes('شناسه عمومی')
const isImported = normalized.includes('وارداتی') // const isImported = normalized.includes('وارداتی')
const isDomestic = !isImported // const isDomestic = !isImported
return { isPublic, isDomestic } // return { isPublic, isDomestic }
} // }
function parseVat(vatValue: string) { // function parseVat(vatValue: string) {
const parsed = Number(vatValue || '0') // const parsed = Number(vatValue || '0')
if (!Number.isFinite(parsed)) return 0 // if (!Number.isFinite(parsed)) return 0
return parsed // return parsed
} // }
async function main() { // async function main() {
const defaultPath = // const defaultPath =
'/Users/ahasani/Desktop/product_good_2026-05-01T11-35-57_part_1_28812448-0dd9-411c-99e1-334c28d781fe.csv' // '/Users/ahasani/Desktop/product_good_2026-05-01T11-35-57_part_1_28812448-0dd9-411c-99e1-334c28d781fe.csv'
const csvPath = process.argv[2] || defaultPath // const csvPath = process.argv[2] || defaultPath
const absolutePath = path.resolve(csvPath) // const absolutePath = path.resolve(csvPath)
if (!fs.existsSync(absolutePath)) { // if (!fs.existsSync(absolutePath)) {
throw new Error(`CSV file not found: ${absolutePath}`) // throw new Error(`CSV file not found: ${absolutePath}`)
} // }
const raw = fs.readFileSync(absolutePath, 'utf8').replace(/^\uFEFF/, '') // const raw = fs.readFileSync(absolutePath, 'utf8').replace(/^\uFEFF/, '')
const rows = parseCsv(raw) // const rows = parseCsv(raw)
let upserted = 0 // let upserted = 0
let skipped = 0 // let skipped = 0
const guild = await prisma.guild.findFirst({}) // const guild = await prisma.guild.findFirst({})
for (const row of rows) { // for (const row of rows) {
const code = (row.ID || '').trim() // const code = (row.ID || '').trim()
const name = (row.DescriptionOfID || '').trim() // const name = (row.DescriptionOfID || '').trim()
const vat = parseVat((row.Vat || '').trim()) // const vat = parseVat((row.Vat || '').trim())
const type = (row.Type || '').trim() // const type = (row.Type || '').trim()
const { isPublic, isDomestic } = toBooleanFlags(type) // const { isPublic, isDomestic } = toBooleanFlags(type)
if (!code || !name) { // if (!code || !name) {
skipped++ // skipped++
continue // continue
} // }
await prisma.stockKeepingUnits.upsert({ // await prisma.stockKeepingUnits.upsert({
where: { code }, // where: { code },
create: { // create: {
code, // code,
name, // name,
VAT: vat, // VAT: vat,
guild: { // guild: {
connect: { // connect: {
id: guild?.id, // id: guild?.id,
}, // },
}, // },
is_public: isPublic, // is_public: isPublic,
is_domestic: isDomestic, // is_domestic: isDomestic,
}, // },
update: { // update: {
name, // name,
VAT: vat, // VAT: vat,
guild: { // guild: {
connect: { // connect: {
id: guild?.id, // id: guild?.id,
}, // },
}, // },
is_public: isPublic, // is_public: isPublic,
is_domestic: isDomestic, // is_domestic: isDomestic,
}, // },
}) // })
upserted++ // upserted++
} // }
} // }
main() // main()
.catch(error => { // .catch(error => {
console.error(error) // console.error(error)
process.exit(1) // process.exit(1)
}) // })
.finally(async () => { // .finally(async () => {
await prisma.$disconnect() // await prisma.$disconnect()
}) // })
@@ -24,9 +24,10 @@ export const summarySelect: BusinessActivitySelect = {
select: { select: {
activation: { activation: {
select: { select: {
_count: { account_allocations: {
select: { select: {
account_allocations: true, id: true,
account_id: true,
}, },
}, },
}, },
@@ -44,13 +45,18 @@ export const select: BusinessActivitySelect = {
export const mappedData = (businessActivity: any) => { export const mappedData = (businessActivity: any) => {
const { license_activation, ...rest } = businessActivity const { license_activation, ...rest } = businessActivity
const { license, ...license_activation_rest } = license_activation const { license, ...license_activation_rest } = license_activation
const { _count } = license.activation const { account_allocations } = license.activation
console.log('license_activation', license_activation)
return { return {
...rest, ...rest,
license_info: { license_info: {
...license_activation_rest, ...license_activation_rest,
accounts_limit: _count.account_allocations, accounts_limit: account_allocations.length,
allocated_account_count: account_allocations.filter(
allocation => allocation.account_id,
).length,
}, },
} }
} }
+6
View File
@@ -44,6 +44,12 @@ export const summarySelect: SalesInvoiceSelect = {
invoice_number: true, invoice_number: true,
}, },
}, },
referenced_by: {
select: {
id: true,
invoice_number: true,
},
},
} }
export const select: SalesInvoiceSelect = { export const select: SalesInvoiceSelect = {
@@ -1,13 +1,25 @@
import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service' import { SharedSaleInvoiceAccessService } from '@/common/services/saleInvoices/sale-invoice-access.service'
import { PosCorrectionSalesInvoiceDto } from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto' import {
PosCorrectionSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from '@/modules/pos/sales-invoices/dto/create-sales-invoice.dto'
import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '@/modules/tspProviders/sales-invoice-tsp.service'
import { Injectable } from '@nestjs/common' import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import {
CustomerType,
TspProviderRequestType,
TspProviderResponseStatus,
} from 'generated/prisma/enums'
import { SharedSaleInvoiceCreateService } from './sale-invoice-create.service'
@Injectable() @Injectable()
export class SharedSaleInvoiceActionsService { export class SharedSaleInvoiceActionsService {
constructor( constructor(
private prisma: PrismaService,
private readonly salesInvoiceTspService: SalesInvoiceTspService, private readonly salesInvoiceTspService: SalesInvoiceTspService,
private readonly saleInvoiceAccessService: SharedSaleInvoiceAccessService, private readonly saleInvoiceAccessService: SharedSaleInvoiceAccessService,
private sharedSaleInvoiceCreateService: SharedSaleInvoiceCreateService,
) {} ) {}
async send(consumerAccountId: string, posId: string, invoiceId: string) { async send(consumerAccountId: string, posId: string, invoiceId: string) {
@@ -58,16 +70,466 @@ export class SharedSaleInvoiceActionsService {
consumerAccountId, consumerAccountId,
posId, posId,
) )
return this.salesInvoiceTspService.correctionSend(
consumerAccountId, const newInvoice = await this.prisma.$transaction(async tx => {
posId, const relatedInvoice = await tx.salesInvoice.findUnique({
complexId, where: {
businessId, id: invoiceId,
invoiceId, pos_id: posId,
data, },
select: {
id: true,
invoice_date: true,
settlement_type: true,
customer_id: true,
main_id: true,
last_tsp_status: true,
referenced_by: {
select: {
id: true,
},
},
customer: {
select: {
type: true,
},
},
total_amount: true,
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
unit_price: true,
total_amount: true,
discount_amount: true,
tax_amount: true,
payload: true,
notes: true,
},
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
if (relatedInvoice.last_tsp_status !== TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException('امکان اصلاح این صورت‌حساب وجود ندارد.')
}
if (relatedInvoice.referenced_by) {
throw new BadRequestException(
'این صورت‌حساب قبلا با یک صورت‌حساب دیگر جایگزین شده است.',
) )
} }
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const originalInvoiceDate = this.normalizeInvoiceDate(relatedInvoice.invoice_date)
const originalItemsByKey = new Map(
relatedInvoice.items.map(item => [this.getItemKey(item), item]),
)
const usedItemKeys = new Set<string>()
const normalizedItems = data.items.map(item => {
const itemKey = this.getItemKey(item)
if (!itemKey) {
throw new BadRequestException('هر قلم باید شناسه کالا یا خدمت داشته باشد.')
}
usedItemKeys.add(itemKey)
const originalItem = originalItemsByKey.get(itemKey)
if (!originalItem) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل اصلاح هستند.',
)
}
const originalQuantity = Number(originalItem.quantity)
const requestedQuantity = Number(item.quantity)
const originalUnitPrice = Number(originalItem.unit_price)
const requestedUnitPrice = Number(item.unit_price)
const originalTotalAmount = this.roundAmount(Number(originalItem.total_amount))
const requestedTotalAmount = this.roundAmount(Number(item.total_amount))
if (requestedQuantity <= 0) {
throw new BadRequestException(
'تعداد/مقدار اقلام اصلاحی باید بیشتر از صفر باشد.',
)
}
// if (requestedUnitPrice < originalUnitPrice) {
// throw new BadRequestException(
// 'مبلغ واحد اقلام اصلاحی نمی‌تواند کمتر از صورت‌حساب اصلی باشد.',
// )
// }
return {
invoice_id: originalItem.id,
good_id: originalItem.good_id,
service_id: originalItem.service_id || undefined,
quantity: requestedQuantity,
unit_price: requestedUnitPrice,
total_amount: requestedTotalAmount,
discount_amount: this.roundAmount(Number(item.discount_amount || 0)),
tax_amount: this.roundAmount(Number(item.tax_amount || 0)),
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
notes: item.notes || undefined,
}
})
if (usedItemKeys.size !== relatedInvoice.items.length) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل اصلاح هستند.',
)
}
const hasChangedQuantity = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem => this.getItemKey(relatedItem) === this.getItemKey(item),
)
if (!originalItem) {
return false
}
return Number(originalItem.quantity) !== Number(item.quantity)
})
const hasChangedAmount = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem => this.getItemKey(relatedItem) === this.getItemKey(item),
)
if (!originalItem) {
return false
}
return (
this.roundAmount(Number(originalItem.total_amount)) !==
this.roundAmount(Number(item.total_amount))
)
})
const hasChangedInvoiceDate = normalizedInvoiceDate !== originalInvoiceDate
if (!hasChangedQuantity && !hasChangedAmount && !hasChangedInvoiceDate) {
throw new BadRequestException('مقداری تغییر نکرده است.')
}
const requestedTotalAmount = this.roundAmount(Number(data.total_amount))
const originalTotalAmount = this.roundAmount(Number(relatedInvoice.total_amount))
const totalDiff = this.roundAmount(requestedTotalAmount - originalTotalAmount)
const paymentsAmount = this.roundAmount(this.getPaymentsAmount(data.payments))
if (totalDiff > 0 && paymentsAmount !== totalDiff) {
throw new BadRequestException(
'جمع پرداختی باید برابر با اختلاف مبلغ صورت‌حساب اصلاحی و صورت‌حساب مرجع باشد.',
)
}
if (totalDiff === 0 && paymentsAmount !== 0) {
throw new BadRequestException('در صورت عدم افزایش مبلغ، پرداختی نباید ثبت شود.')
}
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
invoice_date: new Date(normalizedInvoiceDate),
items: normalizedItems as any,
total_amount: requestedTotalAmount,
discount_amount: this.roundAmount(Number(data.discount_amount)),
tax_amount: this.roundAmount(Number(data.tax_amount)),
customer_id: relatedInvoice.customer_id || undefined,
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
settlement_type: relatedInvoice.settlement_type,
payments: data.payments,
} as any,
businessId,
complexId,
posId,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION,
})
await tx.salesInvoice.update({
where: {
id: relatedInvoice.id,
},
data: {
referenced_by: {
connect: {
id: newInvoice.id,
},
},
},
})
return newInvoice
})
return this.salesInvoiceTspService.correctionSend(posId, newInvoice.id)
}
async return(
data: PosReturnSalesInvoiceDto,
consumerAccountId: string,
pos_id: string,
complex_id: string,
business_id: string,
invoiceId: string,
) {
await this.saleInvoiceAccessService.getConsumerIdWithPosAccess(
consumerAccountId,
pos_id,
)
const newInvoice = await this.prisma.$transaction(async tx => {
const relatedInvoice = await tx.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id,
},
select: {
id: true,
type: true,
invoice_date: true,
settlement_type: true,
customer_id: true,
main_id: true,
last_tsp_status: true,
referenced_by: {
select: {
id: true,
},
},
customer: {
select: {
type: true,
},
},
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
unit_price: true,
total_amount: true,
discount_amount: true,
tax_amount: true,
payload: true,
notes: true,
},
},
},
})
if (!relatedInvoice) {
throw new NotFoundException('صورت‌حساب مورد نظر شما یافت نشد.')
}
if (relatedInvoice.last_tsp_status !== TspProviderResponseStatus.SUCCESS) {
throw new BadRequestException('امکان برگشت از خرید روی این صورت‌حساب وجود ندارد.')
}
if (relatedInvoice.referenced_by) {
throw new BadRequestException(
'این صورت‌حساب قبلا با یک صورت‌حساب دیگر جایگزین شده است.',
)
}
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const originalInvoiceDate = this.normalizeInvoiceDate(relatedInvoice.invoice_date)
const originalItemsByKey = new Map(
relatedInvoice.items.map(item => [this.getReturnItemKey(item), item]),
)
const usedItemKeys = new Set<string>()
const normalizedItems = data.items.map(item => {
const itemKey = this.getReturnItemKey(item)
if (!itemKey) {
throw new BadRequestException('هر قلم باید شناسه کالا یا خدمت داشته باشد.')
}
// if (usedItemKeys.has(itemKey)) {
// throw new BadRequestException('اقلام تکراری در صورت‌حساب بازگشتی مجاز نیستند.')
// }
usedItemKeys.add(itemKey)
const originalItem = originalItemsByKey.get(itemKey)
if (!originalItem) {
throw new BadRequestException(
'فقط اقلام موجود در صورت‌حساب اصلی قابل بازگشت هستند.',
)
}
const originalQuantity = Number(originalItem.quantity)
const requestedQuantity = Number(item.quantity)
if (requestedQuantity <= 0) {
throw new BadRequestException(
'تعداد/مقدار اقلام بازگشتی باید بیشتر از صفر باشد.',
)
}
if (requestedQuantity > originalQuantity) {
throw new BadRequestException(
'تعداد/مقدار اقلام بازگشتی نمی‌تواند از مقدار صورت‌حساب اصلی بیشتر باشد.',
)
}
const quantityRatio = requestedQuantity / originalQuantity
return {
invoice_id: originalItem.id,
good_id: originalItem.good_id,
service_id: originalItem.service_id || undefined,
quantity: requestedQuantity,
unit_price: Number(originalItem.unit_price),
total_amount: this.roundAmount(
Number(originalItem.total_amount) * quantityRatio,
),
discount_amount: this.roundAmount(
Number(originalItem.discount_amount || 0) * quantityRatio,
),
tax_amount: this.roundAmount(
Number(originalItem.tax_amount || 0) * quantityRatio,
),
payload: originalItem.payload
? JSON.parse(JSON.stringify(originalItem.payload))
: undefined,
notes: originalItem.notes || undefined,
}
})
if (!normalizedItems.length) {
throw new BadRequestException(
'حداقل یک قلم باید در صورت‌حساب بازگشتی باقی بماند.',
)
}
const hasRemovedItem = relatedInvoice.items.some(
item => !usedItemKeys.has(this.getReturnItemKey(item)),
)
const hasChangedQuantity = normalizedItems.some(item => {
const originalItem = relatedInvoice.items.find(
relatedItem =>
this.getReturnItemKey(relatedItem) === this.getReturnItemKey(item),
)
if (!originalItem) {
return false
}
return Number(originalItem.quantity) !== Number(item.quantity)
})
const hasChangedInvoiceDate = normalizedInvoiceDate !== originalInvoiceDate
if (!hasRemovedItem && !hasChangedQuantity && !hasChangedInvoiceDate) {
throw new BadRequestException('مقداری تغییر نکرده است.')
}
const totalAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.total_amount),
0,
)
const discountAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.discount_amount || 0),
0,
)
const taxAmount = normalizedItems.reduce(
(sum, item) => sum + Number(item.tax_amount || 0),
0,
)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({
tx,
data: {
invoice_date: new Date(normalizedInvoiceDate),
items: normalizedItems as any,
total_amount: totalAmount,
discount_amount: discountAmount,
tax_amount: taxAmount,
customer_id: relatedInvoice.customer_id || undefined,
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
settlement_type: relatedInvoice.settlement_type,
payments: undefined,
} as any,
businessId: business_id,
complexId: complex_id,
posId: pos_id,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.RETURN,
})
await tx.salesInvoice.update({
where: {
id: relatedInvoice.id,
},
data: {
referenced_by: {
connect: {
id: newInvoice.id,
},
},
},
})
return newInvoice
})
return this.salesInvoiceTspService.returnFromSaleSend(
pos_id,
business_id,
newInvoice.id,
)
}
private normalizeInvoiceDate(invoiceDate: Date | string) {
return new Date(invoiceDate).toISOString()
}
private getItemKey(item: { good_id?: string | null; service_id?: string | null }) {
return item.good_id || item.service_id || ''
}
private getPaymentsAmount(payments?: {
terminals?: { amount?: number }
cash?: number
set_off?: number
card?: number
bank?: number
check?: number
other?: number
}) {
return (
Number(payments?.terminals?.amount || 0) +
Number(payments?.cash || 0) +
Number(payments?.set_off || 0) +
Number(payments?.card || 0) +
Number(payments?.bank || 0) +
Number(payments?.check || 0) +
Number(payments?.other || 0)
)
}
private roundAmount(amount: number) {
return Number(amount.toFixed(2))
}
async inquiry(consumerAccountId: string, posId: string, invoiceId: string) { async inquiry(consumerAccountId: string, posId: string, invoiceId: string) {
const consumerId = await this.saleInvoiceAccessService.getConsumerIdWithPosAccess( const consumerId = await this.saleInvoiceAccessService.getConsumerIdWithPosAccess(
consumerAccountId, consumerAccountId,
@@ -75,4 +537,11 @@ export class SharedSaleInvoiceActionsService {
) )
return this.salesInvoiceTspService.get(invoiceId, posId, consumerId) return this.salesInvoiceTspService.get(invoiceId, posId, consumerId)
} }
private getReturnItemKey(item: {
good_id?: string | null
service_id?: string | null
}) {
return item.good_id || item.service_id || ''
}
} }
@@ -83,7 +83,7 @@ export class SharedCreateTerminalPayment {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ required: true }) @ApiProperty({ required: true })
terminalId: string terminal_id: string
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@@ -196,6 +196,11 @@ export class SharedCreateSalesInvoiceDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
items: SharedCreateSalesInvoiceItemDto[] items: SharedCreateSalesInvoiceItemDto[]
@ApiProperty({ required: false })
@IsOptional()
@IsString()
ref_invoice_id?: string
@ApiProperty({ required: false }) @ApiProperty({ required: false })
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -230,5 +235,13 @@ export class SharedCreateSalesInvoiceDto {
export class SharedCorrectionSalesInvoiceDto extends OmitType( export class SharedCorrectionSalesInvoiceDto extends OmitType(
SharedCreateSalesInvoiceDto, SharedCreateSalesInvoiceDto,
['customer', 'customer_id', 'send_to_tsp', 'customer_type', 'settlement_type'], ['customer', 'customer_id', 'customer_type', 'settlement_type'],
) {} ) {}
export class SharedReturnSalesInvoiceDto extends OmitType(SharedCreateSalesInvoiceDto, [
'customer',
'customer_id',
'customer_type',
'settlement_type',
'payments',
]) {}
@@ -23,6 +23,7 @@ interface TerminalPaymentInfo {
interface NormalizedPayment { interface NormalizedPayment {
method: PaymentMethodType method: PaymentMethodType
amount: number amount: number
terminalInfo?: TerminalPaymentInfo
} }
interface CreateSharedSaleInvoiceInput { interface CreateSharedSaleInvoiceInput {
@@ -57,10 +58,10 @@ export class SharedSaleInvoiceCreateService {
} = input } = input
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date) const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const { payments, terminalInfo } = this.buildPaymentsData( const payments =
data.payments, type === TspProviderRequestType.ORIGINAL || data.payments
data.total_amount, ? this.buildPaymentsData(data.payments, data.total_amount)
) : []
for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) { for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) {
try { try {
@@ -88,13 +89,14 @@ export class SharedSaleInvoiceCreateService {
select: { ...QUERY_CONSTANTS.SALE_INVOICE.select }, select: { ...QUERY_CONSTANTS.SALE_INVOICE.select },
}) })
if (payments.length) {
await this.createPayments( await this.createPayments(
$tx, $tx,
salesInvoice.id, salesInvoice.id,
payments, payments,
terminalInfo,
normalizedInvoiceDate, normalizedInvoiceDate,
) )
}
return salesInvoice return salesInvoice
}) })
@@ -148,7 +150,7 @@ export class SharedSaleInvoiceCreateService {
} }
const rawPayments = (paymentsData || {}) as Record<string, unknown> const rawPayments = (paymentsData || {}) as Record<string, unknown>
const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined const terminalPayments = rawPayments.terminals as TerminalPaymentInfo[] | undefined
const payments: NormalizedPayment[] = Object.entries(rawPayments) const payments: NormalizedPayment[] = Object.entries(rawPayments)
.filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0) .filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0)
@@ -158,40 +160,25 @@ export class SharedSaleInvoiceCreateService {
})) }))
.filter(payment => payment.method && payment.amount > 0) as NormalizedPayment[] .filter(payment => payment.method && payment.amount > 0) as NormalizedPayment[]
const hasTerminalPayment = payments.some( const hasTerminalPayment = terminalPayments && terminalPayments.length
payment => payment.method === PaymentMethodType.TERMINAL,
)
const nonTerminalTotal = payments
.filter(payment => payment.method !== PaymentMethodType.TERMINAL)
.reduce((sum, payment) => sum + payment.amount, 0)
if (!hasTerminalPayment && terminalInfo) { if (hasTerminalPayment) {
const terminalAmount = for (const terminal of terminalPayments) {
typeof terminalInfo.amount === 'number'
? terminalInfo.amount
: Math.max(0, Number(totalAmount) - nonTerminalTotal)
if (terminalAmount > 0) {
payments.push({ payments.push({
method: PaymentMethodType.TERMINAL, method: PaymentMethodType.TERMINAL,
amount: terminalAmount, amount: terminal.amount,
terminalInfo: terminal,
}) })
} }
} }
this.validatePayments(payments, totalAmount, terminalInfo) //TODO: for correction i need to validate payments with diff of total amount and original invoice amount
// this.validatePayments(payments, totalAmount)
return { return payments
payments,
terminalInfo,
}
} }
private validatePayments( private validatePayments(payments: NormalizedPayment[], totalAmount: number) {
payments: NormalizedPayment[],
totalAmount: number,
terminalInfo?: TerminalPaymentInfo,
) {
const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0) const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0)
const roundedTotalPayments = Number(totalPayments.toFixed(2)) const roundedTotalPayments = Number(totalPayments.toFixed(2))
const roundedTotalAmount = Number(Number(totalAmount).toFixed(2)) const roundedTotalAmount = Number(Number(totalAmount).toFixed(2))
@@ -199,14 +186,6 @@ export class SharedSaleInvoiceCreateService {
if (roundedTotalPayments !== roundedTotalAmount) { if (roundedTotalPayments !== roundedTotalAmount) {
throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل صورت‌حساب باشد.') throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل صورت‌حساب باشد.')
} }
const terminalPayments = payments.filter(
payment => payment.method === PaymentMethodType.TERMINAL,
)
if (terminalPayments.length > 0 && !terminalInfo) {
throw new BadRequestException('برای پرداخت ترمینال اطلاعات ترمینال الزامی است.')
}
} }
private async resolveCustomerId( private async resolveCustomerId(
@@ -277,9 +256,7 @@ export class SharedSaleInvoiceCreateService {
} }
return customerIndividualId return customerIndividualId
} } else if (customer_type === CustomerType.LEGAL && customer?.customer_legal) {
if (customer_type === CustomerType.LEGAL && customer?.customer_legal) {
const { registration_number, economic_code, postal_code } = customer.customer_legal const { registration_number, economic_code, postal_code } = customer.customer_legal
const foundedCustomer = await tx.customerLegal.findFirst({ const foundedCustomer = await tx.customerLegal.findFirst({
where: { where: {
@@ -424,6 +401,7 @@ export class SharedSaleInvoiceCreateService {
customer, customer,
payments, payments,
settlement_type, settlement_type,
send_to_tsp,
...invoiceData ...invoiceData
} = data } = data
@@ -486,6 +464,8 @@ export class SharedSaleInvoiceCreateService {
id: customerId, id: customerId,
}, },
} }
} else if (data.customer?.customer_unknown) {
salesInvoiceData.unknown_customer = data.customer.customer_unknown
} }
if (type !== TspProviderRequestType.ORIGINAL) { if (type !== TspProviderRequestType.ORIGINAL) {
@@ -500,7 +480,7 @@ export class SharedSaleInvoiceCreateService {
return salesInvoiceData return salesInvoiceData
} }
private async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) { async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) {
const latestInvoice = await tx.salesInvoice.findFirst({ const latestInvoice = await tx.salesInvoice.findFirst({
where: { where: {
pos: { pos: {
@@ -541,7 +521,6 @@ export class SharedSaleInvoiceCreateService {
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
invoiceId: string, invoiceId: string,
payments: NormalizedPayment[], payments: NormalizedPayment[],
terminalInfo: TerminalPaymentInfo | undefined,
paidAt: Date, paidAt: Date,
) { ) {
for (const payment of payments) { for (const payment of payments) {
@@ -557,16 +536,27 @@ export class SharedSaleInvoiceCreateService {
}, },
}) })
if (payment.method === PaymentMethodType.TERMINAL && terminalInfo) { if (payment.method === PaymentMethodType.TERMINAL && payment.terminalInfo) {
const {
terminal_id,
stan,
rrn,
transaction_date_time,
customer_card_no,
description,
} = payment.terminalInfo
await tx.salesInvoicePaymentTerminalInfo.create({ await tx.salesInvoicePaymentTerminalInfo.create({
data: { data: {
payment_id: createdPayment.id, payment_id: createdPayment.id,
terminal_id: terminalInfo.terminal_id, terminal_id,
stan: terminalInfo.stan, stan: stan,
rrn: terminalInfo.rrn, rrn: rrn,
transaction_date_time: new Date(terminalInfo.transaction_date_time || ''), transaction_date_time: transaction_date_time
customer_card_no: terminalInfo.customer_card_no || null, ? new Date(transaction_date_time)
description: terminalInfo.description || null, : new Date(),
customer_card_no: '1234567890123456',
// customer_card_no: customer_card_no || null,
description: description || null,
}, },
}) })
} }
+7 -5
View File
@@ -2,18 +2,20 @@ import { PrismaMariaDb } from '@prisma/adapter-mariadb'
import 'dotenv/config' import 'dotenv/config'
import { env } from 'prisma/config' import { env } from 'prisma/config'
import { PrismaClient } from '../generated/prisma/client'
const adapter = new PrismaMariaDb({ const adapter = new PrismaMariaDb({
host: env('DATABASE_HOST'), host: env('DATABASE_HOST'),
user: env('DATABASE_USER'), user: env('DATABASE_USER'),
password: env('DATABASE_PASSWORD'), password: env('DATABASE_PASSWORD'),
database: env('DATABASE_NAME'), database: env('DATABASE_NAME'),
ssl: false,
connectionLimit: 5, connectionLimit: 5,
port: Number(env('DATABASE_PORT')) || 3306, port: Number(env('DATABASE_PORT')) || 3306,
allowPublicKeyRetrieval: true,
ssl: {
rejectUnauthorized: false,
},
connectTimeout: 10000,
}) })
const prisma = new PrismaClient({ adapter }) // const prisma = new PrismaClient({ adapter })
export { prisma } export { adapter as prismaAdapter }
+3 -2
View File
@@ -17,6 +17,8 @@ const cookieParser = require('cookie-parser')
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule) const app = await NestFactory.create(AppModule)
app.enableShutdownHooks()
app.use(cookieParser()) app.use(cookieParser())
const config = new DocumentBuilder() const config = new DocumentBuilder()
@@ -33,6 +35,7 @@ async function bootstrap() {
in: 'header', in: 'header',
}) })
.build() .build()
const documentFactory = () => SwaggerModule.createDocument(app, config) const documentFactory = () => SwaggerModule.createDocument(app, config)
SwaggerModule.setup('swagger', app, documentFactory, { SwaggerModule.setup('swagger', app, documentFactory, {
swaggerOptions: { swaggerOptions: {
@@ -47,14 +50,12 @@ async function bootstrap() {
}, },
}, },
}) })
// Set API prefix and enable URI versioning so alls routes live under /api/v1/* // Set API prefix and enable URI versioning so alls routes live under /api/v1/*
app.setGlobalPrefix('api') app.setGlobalPrefix('api')
app.enableVersioning({ app.enableVersioning({
type: VersioningType.URI, type: VersioningType.URI,
defaultVersion: '1', defaultVersion: '1',
}) })
// Enable CORS. You can set `CORS_ORIGINS` to a comma-separated list of allowed origins. // Enable CORS. You can set `CORS_ORIGINS` to a comma-separated list of allowed origins.
// Defaults include common localhost origins used by front-end dev servers. // Defaults include common localhost origins used by front-end dev servers.
const defaultOrigins: string[] = [] const defaultOrigins: string[] = []
@@ -1,7 +1,6 @@
import { SKUGuildType } from '@/common/enums/enums'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer' import { Type } from 'class-transformer'
import { IsBoolean, IsEnum, IsNumber, IsString, Min } from 'class-validator' import { IsBoolean, IsNumber, IsString, Min } from 'class-validator'
export class CreateStockKeepingUnitDto { export class CreateStockKeepingUnitDto {
@ApiProperty() @ApiProperty()
@@ -18,9 +17,9 @@ export class CreateStockKeepingUnitDto {
@Min(0) @Min(0)
VAT: number VAT: number
@ApiPropertyOptional({ enum: SKUGuildType, default: SKUGuildType.GOLD }) // @ApiPropertyOptional({ enum: SKUGuildType, default: SKUGuildType.GOLD })
@IsEnum(SKUGuildType) // @IsEnum(SKUGuildType)
type: SKUGuildType = SKUGuildType.GOLD // type: SKUGuildType = SKUGuildType.GOLD
@ApiPropertyOptional({ default: true }) @ApiPropertyOptional({ default: true })
@IsBoolean() @IsBoolean()
@@ -105,7 +105,9 @@ export class PartnerAccountChargeTransactionService {
try { try {
createdTransaction = await tx.partnerAccountQuotaChargeTransaction.create({ createdTransaction = await tx.partnerAccountQuotaChargeTransaction.create({
data: { data: {
activation_expires_at: data.activated_expires_at, activation_expires_at: data.activated_expires_at
? new Date(data.activated_expires_at).toISOString()
: new Date().toISOString(),
tracking_code: generateTrackingCode('AQC', this.TRACKING_CODE_LENGTH), tracking_code: generateTrackingCode('AQC', this.TRACKING_CODE_LENGTH),
purchased_count: data.quantity, purchased_count: data.quantity,
partner: { connect: { id: partner_id } }, partner: { connect: { id: partner_id } },
@@ -121,7 +121,9 @@ export class PartnerLicenseChargeTransactionService {
try { try {
createdTransaction = await tx.licenseChargeTransaction.create({ createdTransaction = await tx.licenseChargeTransaction.create({
data: { data: {
activation_expires_at: data.activated_expires_at, activation_expires_at: data.activated_expires_at
? new Date(data.activated_expires_at).toISOString()
: new Date().toISOString(),
tracking_code: generateTrackingCode('LIC', this.TRACKING_CODE_LENGTH), tracking_code: generateTrackingCode('LIC', this.TRACKING_CODE_LENGTH),
purchased_count: data.quantity, purchased_count: data.quantity,
partner: { connect: { id: partner_id } }, partner: { connect: { id: partner_id } },
@@ -4,6 +4,7 @@ import type { IPosPayload } from '@/common/models'
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common' import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger' import { ApiTags } from '@nestjs/swagger'
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto' import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
import { PosCorrectionSalesInvoiceDto } from '../../pos/sales-invoices/dto/create-sales-invoice.dto'
import type { import type {
SaleInvoicesServiceFindAllResponseDto, SaleInvoicesServiceFindAllResponseDto,
SaleInvoicesServiceFindOneResponseDto, SaleInvoicesServiceFindOneResponseDto,
@@ -59,4 +60,18 @@ export class StatisticsController {
revoke(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) { revoke(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
return this.service.revoke(id, posInfo) return this.service.revoke(id, posInfo)
} }
@Post(':id/correction')
correction(
@Param('id') id: string,
@PosInfo() posInfo: IPosPayload,
@Body() data: PosCorrectionSalesInvoiceDto,
) {
return this.service.correction(id, posInfo, data)
}
@Post(':id/return')
return(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) {
return this.service.return(id, posInfo)
}
} }
@@ -13,6 +13,7 @@ import {
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { PosCorrectionSalesInvoiceDto } from '../../pos/sales-invoices/dto/create-sales-invoice.dto'
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto' import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
@@ -177,6 +178,39 @@ export class SaleInvoicesService {
return ResponseMapper.single(invoice) return ResponseMapper.single(invoice)
} }
async correction(
invoiceId: string,
posInfo: IPosPayload,
data: PosCorrectionSalesInvoiceDto,
) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.correction(
data,
consumer_account_id,
pos_id,
complex_id,
business_id,
invoiceId,
)
return ResponseMapper.single(invoice)
}
async return(invoiceId: string, posInfo: IPosPayload) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.revoke(
consumer_account_id,
pos_id,
complex_id,
business_id,
invoiceId,
)
return ResponseMapper.single(invoice)
}
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) { async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
const invoice = await this.sharedSaleInvoiceActionsService.inquiry( const invoice = await this.sharedSaleInvoiceActionsService.inquiry(
consumer_account_id, consumer_account_id,
@@ -10,13 +10,13 @@ export class StatisticsService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
private readonly invoiceMapper = (invoice: any) => { private readonly invoiceMapper = (invoice: any) => {
const { tsp_attempts, ...rest } = invoice || {} const { last_tsp_status, ...rest } = invoice || {}
return { return {
...rest, ...rest,
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
} }
+2 -2
View File
@@ -25,7 +25,7 @@ import {
TspProviderType, TspProviderType,
} from '@/generated/prisma/enums' } from '@/generated/prisma/enums'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { GoldKarat, SKUGuildType, TspProviderCustomerType } from 'common/enums/enums' import { GoldKarat, TspProviderCustomerType } from 'common/enums/enums'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@Injectable() @Injectable()
@@ -80,7 +80,7 @@ export class EnumsService {
TspProviderCustomerType, TspProviderCustomerType,
'TspProviderCustomerType', 'TspProviderCustomerType',
), ),
SKUGuildType: this.prepareData(SKUGuildType, 'SKUGuildType'), // SKUGuildType: this.prepareData(SKUGuildType, 'SKUGuildType'),
InvoiceTemplateType: this.prepareData(InvoiceTemplateType, 'InvoiceTemplateType'), InvoiceTemplateType: this.prepareData(InvoiceTemplateType, 'InvoiceTemplateType'),
} }
} }
+2 -2
View File
@@ -115,14 +115,14 @@ export class InvoicesService {
private readonly where = () => ({}) private readonly where = () => ({})
private readonly invoiceMapper = (invoice: any) => { private readonly invoiceMapper = (invoice: any) => {
const { tsp_attempts, type, ...rest } = invoice || {} const { last_tsp_status, type, ...rest } = invoice || {}
return { return {
...rest, ...rest,
type: translateEnumValue('TspProviderRequestType', type), type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
} }
@@ -1,10 +1,14 @@
import { RedisKeyMaker } from '@/common/utils' import { RedisKeyMaker } from '@/common/utils'
import { PosCacheInvalidationService } from '@/modules/pos/cache/pos-cache-invalidation.service'
import { RedisService } from '@/redis/redis.service' import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
@Injectable() @Injectable()
export class PartnersCacheInvalidationService { export class PartnersCacheInvalidationService {
constructor(private readonly redisService: RedisService) {} constructor(
private readonly redisService: RedisService,
private readonly PosCacheInvalidationService: PosCacheInvalidationService,
) {}
async invalidatePartnerSummary(partnerId: string): Promise<void> { async invalidatePartnerSummary(partnerId: string): Promise<void> {
await this.invalidatePartnersList() await this.invalidatePartnersList()
@@ -6,6 +6,10 @@ import { Injectable } from '@nestjs/common'
export class PosCacheInvalidationService { export class PosCacheInvalidationService {
constructor(private readonly redisService: RedisService) {} constructor(private readonly redisService: RedisService) {}
async invalidatePosInfo(posId: string): Promise<void> {
this.redisService.delete(PosKeyMaker.info(posId))
}
async invalidatePosMiddleware(token: string): Promise<void> { async invalidatePosMiddleware(token: string): Promise<void> {
this.redisService.delete(PosKeyMaker.middleware(token)) this.redisService.delete(PosKeyMaker.middleware(token))
} }
@@ -2,6 +2,51 @@ import {
SharedCorrectionSalesInvoiceDto, SharedCorrectionSalesInvoiceDto,
SharedCreateSalesInvoiceDto, SharedCreateSalesInvoiceDto,
} from '@/common/services/saleInvoices/sale-invoice-create.dto' } from '@/common/services/saleInvoices/sale-invoice-create.dto'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNumber,
IsOptional,
IsString,
Min,
ValidateNested,
} from 'class-validator'
export class PosCreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {} export class PosCreateSalesInvoiceDto extends SharedCreateSalesInvoiceDto {}
export class PosCorrectionSalesInvoiceDto extends SharedCorrectionSalesInvoiceDto {} export class PosCorrectionSalesInvoiceDto extends SharedCorrectionSalesInvoiceDto {}
export class PosReturnSalesInvoiceItemDto {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
good_id?: string
@ApiProperty({ required: false })
@IsOptional()
@IsString()
service_id?: string
@ApiProperty({ required: true, default: 1 })
@IsNumber()
@Min(1)
quantity: number
}
export class PosReturnSalesInvoiceDto {
@ApiProperty({ required: true })
@IsDateString(
{ strict: true },
{ message: 'invoice_date must be a valid ISO-8601 string' },
)
invoice_date: Date
@ApiProperty({ required: true, type: [PosReturnSalesInvoiceItemDto] })
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => PosReturnSalesInvoiceItemDto)
items: PosReturnSalesInvoiceItemDto[]
}
@@ -5,6 +5,7 @@ import type { IPosPayload } from '@/common/models/posPayload.model'
import { import {
PosCorrectionSalesInvoiceDto, PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto, PosCreateSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from './dto/create-sales-invoice.dto' } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto' import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
import { SalesInvoicesService } from './sales-invoices.service' import { SalesInvoicesService } from './sales-invoices.service'
@@ -60,9 +61,13 @@ export class SalesInvoicesController {
) { ) {
return this.salesInvoicesService.correction(id, posInfo, data) return this.salesInvoicesService.correction(id, posInfo, data)
} }
@Post(':id/return') @Post(':id/return_from_sale')
return(@Param('id') id: string, @PosInfo() posInfo: IPosPayload) { return(
return this.salesInvoicesService.return(id, posInfo) @Param('id') id: string,
@PosInfo() posInfo: IPosPayload,
@Body() data: PosReturnSalesInvoiceDto,
) {
return this.salesInvoicesService.return(id, posInfo, data)
} }
// @Post('send/bulk') // @Post('send/bulk')
@@ -13,6 +13,7 @@ import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.ser
import { import {
PosCorrectionSalesInvoiceDto, PosCorrectionSalesInvoiceDto,
PosCreateSalesInvoiceDto, PosCreateSalesInvoiceDto,
PosReturnSalesInvoiceDto,
} from './dto/create-sales-invoice.dto' } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto' import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
@@ -47,13 +48,13 @@ export class SalesInvoicesService {
]) ])
const summaryItems = items.map(invoice => { const summaryItems = items.map(invoice => {
const { tsp_attempts, type, ...rest } = invoice const { last_tsp_status, type, ...rest } = invoice
return { return {
...rest, ...rest,
type: translateEnumValue('TspProviderRequestType', type), type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
} }
}) })
@@ -78,13 +79,13 @@ export class SalesInvoicesService {
}) })
if (invoice) { if (invoice) {
const { tsp_attempts, type, ...rest } = invoice const { last_tsp_status, type, ...rest } = invoice
const mappedInvoice = { const mappedInvoice = {
...rest, ...rest,
type: translateEnumValue('TspProviderRequestType', type), type: translateEnumValue('TspProviderRequestType', type),
status: translateEnumValue( status: translateEnumValue(
'TspProviderResponseStatus', 'TspProviderResponseStatus',
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND, last_tsp_status || TspProviderResponseStatus.NOT_SEND,
), ),
settlement_type: translateEnumValue( settlement_type: translateEnumValue(
'InvoiceSettlementType', 'InvoiceSettlementType',
@@ -172,10 +173,11 @@ export class SalesInvoicesService {
return ResponseMapper.single(invoice) return ResponseMapper.single(invoice)
} }
async return(invoiceId: string, posInfo: IPosPayload) { async return(invoiceId: string, posInfo: IPosPayload, data: PosReturnSalesInvoiceDto) {
const { consumer_account_id, pos_id, business_id, complex_id } = posInfo const { consumer_account_id, pos_id, business_id, complex_id } = posInfo
const invoice = await this.sharedSaleInvoiceActionsService.revoke( const invoice = await this.sharedSaleInvoiceActionsService.return(
data,
consumer_account_id, consumer_account_id,
pos_id, pos_id,
complex_id, complex_id,
@@ -183,7 +185,7 @@ export class SalesInvoicesService {
invoiceId, invoiceId,
) )
return ResponseMapper.single(invoice) return ResponseMapper.create(invoice)
} }
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) { async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
@@ -46,8 +46,9 @@ export class TspProviderCorrectionInvoicePayloadDto {
@IsObject() @IsObject()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => SharedCreateSalesInvoicePaymentsDto) @Type(() => SharedCreateSalesInvoicePaymentsDto)
payments: SharedCreateSalesInvoicePaymentsDto payments?: SharedCreateSalesInvoicePaymentsDto
} }
export class TspProviderCorrectionSendPayloadDto extends TspProviderOriginalSendPayloadDto { export class TspProviderCorrectionSendPayloadDto extends TspProviderOriginalSendPayloadDto {
@ApiProperty({ required: true }) @ApiProperty({ required: true })
@IsString() @IsString()
+1
View File
@@ -3,4 +3,5 @@ export * from './correction.dto'
export * from './get.dto' export * from './get.dto'
export * from './original.dto' export * from './original.dto'
export * from './provider-switch.dto' export * from './provider-switch.dto'
export * from './return.dto'
export * from './revoke.dto' export * from './revoke.dto'
@@ -0,0 +1,48 @@
import { SharedCreateSalesInvoiceItemDto } from '@/common/services/saleInvoices/sale-invoice-create.dto'
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
ArrayMinSize,
IsArray,
IsDateString,
IsNumber,
IsString,
ValidateNested,
} from 'class-validator'
import {
TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto,
} from './original.dto'
export class TspProviderReturnInvoicePayloadDto {
@ApiProperty({ required: true, nullable: true })
@IsDateString()
invoice_date: Date
@ApiProperty({ type: [SharedCreateSalesInvoiceItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => SharedCreateSalesInvoiceItemDto)
@ArrayMinSize(1)
items: SharedCreateSalesInvoiceItemDto[]
@ApiProperty()
@IsNumber()
total_amount: number
@ApiProperty()
@IsNumber()
discount_amount: number
@ApiProperty()
@IsNumber()
tax_amount: number
}
export class TspProviderReturnSendPayloadDto extends TspProviderOriginalSendPayloadDto {
@ApiProperty({ required: true })
@IsString()
last_tax_id: string
}
export class TspProviderReturnSendResponseDto extends TspProviderOriginalResponseDto {}
@@ -6,6 +6,8 @@ import {
TspProviderGetResponseDto, TspProviderGetResponseDto,
TspProviderOriginalResponseDto, TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderReturnSendResponseDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto, TspProviderRevokeResponseDto,
} from './dto' } from './dto'
@@ -48,6 +50,14 @@ export class SalesInvoiceTspSwitchService {
return adapter.correctionSend(payload) return adapter.correctionSend(payload)
} }
async returnFromSale(
payload: TspProviderReturnSendPayloadDto,
): Promise<TspProviderReturnSendResponseDto> {
const adapter = this.resolveSwitch(payload.tsp_provider)
return adapter.returnSend(payload)
}
async sendBulk( async sendBulk(
payloads: TspProviderOriginalSendPayloadDto[], payloads: TspProviderOriginalSendPayloadDto[],
): Promise<TspProviderBulkSendResultDto[]> { ): Promise<TspProviderBulkSendResultDto[]> {
@@ -6,19 +6,15 @@ import {
TspProviderRequestType, TspProviderRequestType,
TspProviderResponseStatus, TspProviderResponseStatus,
} from 'generated/prisma/client' } from 'generated/prisma/client'
import { import { TspProviderActionResponseDto } from './dto'
TspProviderActionResponseDto,
TspProviderCorrectionInvoicePayloadDto,
} from './dto'
import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service' import { SalesInvoiceTspSwitchService } from './sales-invoice-tsp-switch.service'
import { import {
buildCorrectionPayload, buildCorrectionPayload,
buildOriginalPayload, buildOriginalPayload,
buildReturnFromSalePayload,
buildRevokePayload, buildRevokePayload,
getOriginalResendAttemptNumber, getOriginalResendAttemptNumber,
getRelatedInvoiceForCorrection,
onResult, onResult,
trySend,
} from './utils/sales-invoice-tsp.utils' } from './utils/sales-invoice-tsp.utils'
type ItemTspRow = { type ItemTspRow = {
@@ -177,94 +173,68 @@ export class SalesInvoiceTspService {
} }
async correctionSend( async correctionSend(
consumerAccountId: string,
posId: string, posId: string,
complexId: string, invoice_id: string,
businessId: string,
ref_invoice_id: string,
dataToUpdate: TspProviderCorrectionInvoicePayloadDto,
): Promise<TspProviderActionResponseDto> { ): Promise<TspProviderActionResponseDto> {
const [newInvoice, attempt, correctionPayload] = await this.prisma.$transaction( const payload = await buildCorrectionPayload(this.prisma, invoice_id, posId)
async tx => {
const relatedInvoice = await getRelatedInvoiceForCorrection(tx, ref_invoice_id)
const newInvoice = await this.sharedSaleInvoiceCreateService.create({ const attemptNumber = 1
tx,
data: { const invoice = await this.prisma.salesInvoice.update({
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN, where: {
invoice_date: dataToUpdate.invoice_date, id: invoice_id,
payments: dataToUpdate.payments,
items: dataToUpdate.items,
total_amount: dataToUpdate.total_amount,
discount_amount: dataToUpdate.discount_amount,
tax_amount: dataToUpdate.tax_amount,
customer_id: relatedInvoice.customer_id || undefined,
settlement_type: relatedInvoice.settlement_type,
}, },
businessId, data: {
complexId, last_tsp_status: TspProviderResponseStatus.QUEUED,
posId, last_attempt_no: attemptNumber,
consumerAccountId,
main_invoice_id: relatedInvoice.main_id || relatedInvoice.id,
ref_invoice_id: relatedInvoice.id,
type: TspProviderRequestType.CORRECTION, type: TspProviderRequestType.CORRECTION,
}) tsp_attempts: {
create: {
const correctionPayload = await buildCorrectionPayload(tx, newInvoice.id) attempt_no: attemptNumber,
const attempt = await tx.saleInvoiceTspAttempts.create({
data: {
attempt_no: 1,
invoice_id: newInvoice.id,
message: 'در حال ارسال به سامانه مالیاتی...',
status: TspProviderResponseStatus.QUEUED,
sent_at: new Date().toISOString(),
raw_request_payload: JSON.parse(JSON.stringify(correctionPayload)),
provider_request_payload: {}, provider_request_payload: {},
status: TspProviderResponseStatus.QUEUED,
raw_request_payload: JSON.parse(JSON.stringify(payload)),
sent_at: new Date().toISOString(),
message: 'در حال ارسال به سامانه مالیاتی...',
},
},
}, },
}) })
return [newInvoice, attempt, correctionPayload] const result = await this.tspSwitchService.send(payload)
}, return await onResult(this.prisma, result, invoice.id)
)
const result = await this.runProviderCall(() =>
trySend(this.tspSwitchService, correctionPayload),
)
if (result?.hasError) {
result.provider_request_payload =
result.provider_request_payload || JSON.parse(JSON.stringify(correctionPayload))
result.sent_at = result.sent_at || new Date().toISOString()
result.received_at = result.received_at || new Date().toISOString()
} }
return onResult(this.prisma, result, attempt.id) async returnFromSaleSend(
pos_id: string,
business_id: string,
invoice_id: string,
): Promise<TspProviderActionResponseDto> {
const payload = await buildReturnFromSalePayload(this.prisma, invoice_id, pos_id)
// const countByGoodId = (goodIds: string[]): Map<string, number> => { const attemptNumber = 1
// const counts = new Map<string, number>()
// for (const goodId of goodIds) {
// counts.set(goodId, (counts.get(goodId) ?? 0) + 1)
// }
// return counts
// }
// const updatedGoodIds = dataToUpdate.items.map(item => item.good_id!) const invoice = await this.prisma.salesInvoice.update({
// const previousGoodIds = lastAttempt.invoice.items.map(item => item.good_id) where: {
id: invoice_id,
// const updatedCounts = countByGoodId(updatedGoodIds) },
// const previousCounts = countByGoodId(previousGoodIds) data: {
last_tsp_status: TspProviderResponseStatus.QUEUED,
// let isBackFromSale = false last_attempt_no: attemptNumber,
// if (updatedCounts.size !== previousCounts.size) { tsp_attempts: {
// isBackFromSale = true create: {
// } else { attempt_no: attemptNumber,
// for (const [goodId, count] of updatedCounts) { provider_request_payload: {},
// if (previousCounts.get(goodId) !== count) { status: TspProviderResponseStatus.QUEUED,
// isBackFromSale = true raw_request_payload: JSON.parse(JSON.stringify(payload)),
// break sent_at: new Date().toISOString(),
// } message: 'در حال ارسال به سامانه مالیاتی...',
// } },
// } },
},
})
const result = await this.tspSwitchService.returnFromSale(payload)
return await onResult(this.prisma, result, invoice.id)
} }
async revoke( async revoke(
@@ -317,71 +287,72 @@ export class SalesInvoiceTspService {
) )
} }
const payments = relatedInvoice.payments.reduce( // const payments = relatedInvoice.payments.reduce(
(acc, payment) => { // (acc, payment) => {
const amount = Number(payment.amount) // const amount = Number(payment.amount)
switch (payment.payment_method) { // switch (payment.payment_method) {
case 'CASH': // case 'CASH':
acc.cash = (acc.cash || 0) + amount // acc.cash = (acc.cash || 0) + amount
break // break
case 'SET_OFF': // case 'SET_OFF':
acc.set_off = (acc.set_off || 0) + amount // acc.set_off = (acc.set_off || 0) + amount
break // break
case 'CARD': // case 'CARD':
acc.card = (acc.card || 0) + amount // acc.card = (acc.card || 0) + amount
break // break
case 'BANK': // case 'BANK':
acc.bank = (acc.bank || 0) + amount // acc.bank = (acc.bank || 0) + amount
break // break
case 'CHEQUE': // case 'CHEQUE':
acc.check = (acc.check || 0) + amount // acc.check = (acc.check || 0) + amount
break // break
case 'OTHER': // case 'OTHER':
acc.other = (acc.other || 0) + amount // acc.other = (acc.other || 0) + amount
break // break
case 'TERMINAL': // case 'TERMINAL':
acc.terminals = payment.terminal_info // acc.terminals = payment.terminal_info
? { // ? {
amount, // amount,
terminalId: payment.terminal_info.terminal_id, // terminal_id: payment.terminal_info.terminal_id,
stan: payment.terminal_info.stan, // stan: payment.terminal_info.stan,
rrn: payment.terminal_info.rrn, // rrn: payment.terminal_info.rrn,
customer_card_no: // customer_card_no:
payment.terminal_info.customer_card_no || undefined, // payment.terminal_info.customer_card_no || undefined,
transaction_date_time: payment.terminal_info.transaction_date_time, // transaction_date_time: payment.terminal_info.transaction_date_time,
description: payment.terminal_info.description || undefined, // description: payment.terminal_info.description || undefined,
} // }
: acc.terminals // : acc.terminals
break // break
default: // default:
break // break
} // }
return acc // return acc
}, // },
{} as { // {} as {
terminals?: { // terminals?: {
amount?: number // amount?: number
terminalId: string // terminalId: string
stan: string // stan: string
rrn: string // rrn: string
customer_card_no?: string // customer_card_no?: string
transaction_date_time: Date // transaction_date_time: Date
description?: string // description?: string
} // }
cash?: number // cash?: number
set_off?: number // set_off?: number
card?: number // card?: number
bank?: number // bank?: number
check?: number // check?: number
other?: number // other?: number
}, // },
) // )
const newInvoice = await this.sharedSaleInvoiceCreateService.create({ const newInvoice = await this.sharedSaleInvoiceCreateService.create({
data: { data: {
customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN, customer_type: relatedInvoice.customer?.type || CustomerType.UNKNOWN,
invoice_date: new Date(), invoice_date: new Date(),
payments, // @ts-ignore
payments: undefined,
settlement_type: relatedInvoice.settlement_type, settlement_type: relatedInvoice.settlement_type,
items: relatedInvoice.items.map(item => ({ items: relatedInvoice.items.map(item => ({
unit_price: Number(item.unit_price), unit_price: Number(item.unit_price),
@@ -2,4 +2,5 @@ export * from './common.dto'
export * from './correction.dto' export * from './correction.dto'
export * from './get.dto' export * from './get.dto'
export * from './original.dto' export * from './original.dto'
export * from './returnFromSale.dto'
export * from './revoke.dto' export * from './revoke.dto'
@@ -0,0 +1,60 @@
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsArray, IsString, Length, ValidateNested } from 'class-validator'
import {
NamaProviderCommonBodyDto,
NamaProviderCommonHeaderDto,
NamaProviderPaymentInfoDto,
NamaProviderResponseDto,
} from './common.dto'
export class NamaProviderReturnHeaderDto extends NamaProviderCommonHeaderDto {
@ApiProperty({ required: true, description: 'موضوع صورتحساب (3- برگشت از فروش)' })
@IsString()
ins: '3'
@ApiProperty({
required: true,
description: 'شناسه مالیاتی صورت‌حساب مرجع',
minLength: 22,
maxLength: 22,
})
@IsString()
@Length(22, 22)
irtaxid: string
}
export class NamaProviderReturnBodyItemDto extends NamaProviderCommonBodyDto {}
export class NamaProviderReturnRequestDto {
@ApiProperty({ type: [NamaProviderReturnBodyItemDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => NamaProviderReturnBodyItemDto)
body: NamaProviderReturnBodyItemDto[]
@ApiProperty({ type: [NamaProviderPaymentInfoDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => NamaProviderPaymentInfoDto)
payment: NamaProviderPaymentInfoDto[]
@ApiProperty({ type: NamaProviderReturnHeaderDto })
@ValidateNested({ each: true })
@Type(() => NamaProviderReturnHeaderDto)
header: NamaProviderReturnHeaderDto
@ApiProperty()
@IsString()
uuid: string
@ApiProperty()
@IsString()
economic_code: string
@ApiProperty()
@IsString()
fiscal_id: string
}
export class NamaProviderReturnResponseDto extends NamaProviderResponseDto {}
@@ -12,6 +12,8 @@ import {
TspProviderGetResponseDto, TspProviderGetResponseDto,
TspProviderOriginalResponseDto, TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderReturnSendResponseDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
TspProviderRevokeResponseDto, TspProviderRevokeResponseDto,
} from '../../dto' } from '../../dto'
@@ -27,6 +29,7 @@ import {
NamaProviderRevokeRequestDto, NamaProviderRevokeRequestDto,
NamaProviderRevokeResponseDto, NamaProviderRevokeResponseDto,
} from './dto' } from './dto'
import { NamaProviderReturnResponseDto } from './dto/returnFromSale.dto'
import { NamaProviderUtils } from './nama-provider.util' import { NamaProviderUtils } from './nama-provider.util'
@Injectable() @Injectable()
@@ -40,6 +43,7 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
private readonly sendPath = '/api/v1/invoices/single-send' private readonly sendPath = '/api/v1/invoices/single-send'
private readonly correctionPath = this.sendPath private readonly correctionPath = this.sendPath
private readonly returnFromSalePath = this.sendPath
private readonly sendBulkPath = '/api/v1/invoices' private readonly sendBulkPath = '/api/v1/invoices'
private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids' private readonly checkBulkStatus = '/api/v1/invoices/check-by-uuids'
private readonly getPath = '/api/v1/invoices' private readonly getPath = '/api/v1/invoices'
@@ -63,6 +67,10 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
return `${this.baseUrl}${this.correctionPath}` return `${this.baseUrl}${this.correctionPath}`
} }
private buildReturnFromSaleUrl() {
return `${this.baseUrl}${this.returnFromSalePath}`
}
private buildGetUrl(invoiceId: string) { private buildGetUrl(invoiceId: string) {
return `${this.baseUrl}${this.getPath}/${invoiceId}` return `${this.baseUrl}${this.getPath}/${invoiceId}`
} }
@@ -176,6 +184,51 @@ export class NamaProviderSwitchAdapter implements IProviderSwitchAdapter {
} }
} }
async returnSend(
payload: TspProviderReturnSendPayloadDto,
): Promise<TspProviderReturnSendResponseDto> {
const mappedRequest = this.namaProviderUtils.mapToNamaReturnDto(payload)
try {
const response = await this.httpClient.request(
this.buildReturnFromSaleUrl(),
{
method: 'POST',
body: JSON.stringify(mappedRequest),
},
this.createRequestInterceptors(payload.token),
)
const providerResponse: NamaProviderReturnResponseDto = await response.json()
this.logger.debug('NAMA provider return response', providerResponse)
const result: TspProviderReturnSendResponseDto = {
invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: !response.ok,
message: providerResponse.message,
provider_response: JSON.parse(JSON.stringify(providerResponse)),
received_at: new Date().toISOString(),
tax_id: providerResponse.tax_id,
status: this.namaProviderUtils.mapResponseStatus(
providerResponse.status as NamaProviderResponseStatus,
),
}
return result
} catch (err) {
this.logger.error('NAMA return from sale send failed', err)
const failure: TspProviderReturnSendResponseDto = {
invoice_id: payload.id,
provider_request_payload: JSON.parse(JSON.stringify(mappedRequest)),
hasError: true,
message: (err as Error).message,
received_at: new Date().toISOString(),
status: TspProviderResponseStatus.SEND_FAILURE,
}
return failure
}
}
async sendBulk( async sendBulk(
payloads: TspProviderOriginalSendPayloadDto[], payloads: TspProviderOriginalSendPayloadDto[],
): Promise<TspProviderBulkSendResultDto[]> { ): Promise<TspProviderBulkSendResultDto[]> {
@@ -12,6 +12,7 @@ import {
PaymentInfoDto, PaymentInfoDto,
TspProviderCorrectionSendPayloadDto, TspProviderCorrectionSendPayloadDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
} from '../../dto' } from '../../dto'
import { import {
@@ -22,6 +23,7 @@ import {
NamaProviderResponseStatus, NamaProviderResponseStatus,
NamaProviderRevokeRequestDto, NamaProviderRevokeRequestDto,
} from './dto' } from './dto'
import { NamaProviderReturnRequestDto } from './dto/returnFromSale.dto'
export class NamaProviderUtils { export class NamaProviderUtils {
mapResponseStatus(status: NamaProviderResponseStatus): TspProviderResponseStatus { mapResponseStatus(status: NamaProviderResponseStatus): TspProviderResponseStatus {
@@ -68,7 +70,7 @@ export class NamaProviderUtils {
sstid: item.sku, sstid: item.sku,
vra: item.sku_vat, vra: item.sku_vat,
fee: String(item.unit_price), fee: String(item.unit_price),
dis: String(item.discount_amount), dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit, mu: item.measure_unit,
am: String(item.quantity), am: String(item.quantity),
consfee: '0', consfee: '0',
@@ -120,7 +122,58 @@ export class NamaProviderUtils {
sstid: item.sku, sstid: item.sku,
vra: item.sku_vat, vra: item.sku_vat,
fee: String(item.unit_price), fee: String(item.unit_price),
dis: String(item.discount_amount), dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit,
am: String(item.quantity),
consfee: '0',
bros: '0',
spro: '0',
}
if (item.gold_type_payload) {
const { profit = '0', commission = '0', wages = '0' } = item.gold_type_payload
data = {
...data,
consfee: wages,
bros: commission,
spro: profit,
tcpbs: (Number(profit) + Number(commission) + Number(wages)).toString(),
}
}
return data
}),
}
}
mapToNamaReturnDto(
payload: TspProviderReturnSendPayloadDto,
): NamaProviderReturnRequestDto {
return {
uuid: payload.id,
economic_code: payload.economic_code,
fiscal_id: payload.fiscal_id,
payment: this.mapPayments(payload.payments),
header: {
ins: this.mapRequestType(TspProviderRequestType.RETURN),
inp: this.mapInvoiceTemplate(payload.template),
inty: this.mapTspProviderCustomerType(payload.customer?.type),
inno: payload.invoice_number.toString(),
tins: '',
indatim: payload.invoice_date.getTime() + '',
irtaxid: payload.last_tax_id,
...this.mapSettlementBased(
payload.settlement_type,
payload.payments,
payload.total_amount,
),
...this.mapCustomerInfo(payload.customer),
},
body: payload.items.map(item => {
let data: NamaProviderOriginalBodyItemDto = {
sstid: item.sku,
vra: item.sku_vat,
fee: String(item.unit_price),
dis: String(parseInt(item.discount_amount + '')),
mu: item.measure_unit, mu: item.measure_unit,
am: String(item.quantity), am: String(item.quantity),
consfee: '0', consfee: '0',
@@ -297,6 +350,9 @@ export class NamaProviderUtils {
} }
private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] { private mapPayments(payments: PaymentInfoDto[]): NamaProviderPaymentInfoDto[] {
if (!payments) {
return []
}
return payments.map(payment => ({ return payments.map(payment => ({
pmt: this.mapPaymentMethod(payment.payment_method), pmt: this.mapPaymentMethod(payment.payment_method),
pv: payment.amount, pv: payment.amount,
@@ -9,12 +9,13 @@ import {
} from 'generated/prisma/client' } from 'generated/prisma/client'
import { import {
goldTypePayload, goldTypePayload,
TspProviderCorrectionSendPayloadDto,
TspProviderGetResponseDto, TspProviderGetResponseDto,
TspProviderOriginalResponseDto, TspProviderOriginalResponseDto,
TspProviderOriginalSendPayloadDto, TspProviderOriginalSendPayloadDto,
TspProviderReturnSendPayloadDto,
TspProviderRevokePayloadDto, TspProviderRevokePayloadDto,
} from '../dto' } from '../dto'
import { TspProviderCorrectionSendPayloadDto } from '../dto/correction.dto'
import { TspProviderActionResponseDto } from '../dto/provider-switch.dto' import { TspProviderActionResponseDto } from '../dto/provider-switch.dto'
export async function getOriginalResendAttemptNumber( export async function getOriginalResendAttemptNumber(
@@ -160,10 +161,12 @@ export async function buildRevokePayload(
export async function buildCorrectionPayload( export async function buildCorrectionPayload(
tx: Prisma.TransactionClient, tx: Prisma.TransactionClient,
invoice_id: string, invoice_id: string,
pos_id: string,
): Promise<TspProviderCorrectionSendPayloadDto> { ): Promise<TspProviderCorrectionSendPayloadDto> {
const invoice = await tx.salesInvoice.findUnique({ const invoice = await tx.salesInvoice.findUnique({
where: { where: {
id: invoice_id, id: invoice_id,
pos_id,
}, },
select: { select: {
id: true, id: true,
@@ -323,6 +326,217 @@ export async function buildCorrectionPayload(
} }
} }
export async function buildReturnFromSalePayload(
prisma: PrismaService,
invoiceId: string,
posId: string,
): Promise<TspProviderReturnSendPayloadDto> {
const invoice = await prisma.salesInvoice.findUnique({
where: {
id: invoiceId,
pos_id: posId,
},
select: {
id: true,
code: true,
total_amount: true,
invoice_date: true,
invoice_number: true,
settlement_type: true,
discount_amount: true,
tax_amount: true,
tax_id: true,
items: {
select: {
id: true,
quantity: true,
unit_price: true,
total_amount: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
sku_vat: true,
good_id: true,
service_id: true,
discount_amount: true,
tax_amount: true,
payload: true,
good_snapshot: true,
},
},
pos: {
select: {
complex: {
select: {
business_activity: {
select: {
fiscal_id: true,
economic_code: true,
partner_token: true,
guild: {
select: {
invoice_template: true,
},
},
consumer: {
select: {
legal: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
individual: {
select: {
partner: {
select: {
tsp_provider: true,
},
},
},
},
},
},
},
},
},
},
},
},
unknown_customer: true,
customer: {
select: {
type: true,
individual: true,
legal: true,
},
},
payments: {
select: {
amount: true,
payment_method: true,
paid_at: true,
terminal_info: {
select: {
stan: true,
rrn: true,
transaction_date_time: true,
customer_card_no: true,
terminal_id: true,
},
},
},
},
reference_invoice: {
select: {
tax_id: true,
},
},
},
})
if (!invoice) {
throw new NotFoundException('صورت‌حساب مورد نظر یافت نشد.')
}
if (!invoice.reference_invoice) {
throw new NotFoundException('صورت‌حساب مرجع برای برگشت از فروش یافت نشد.')
}
const {
pos,
id,
invoice_number,
invoice_date,
total_amount,
discount_amount,
tax_amount,
settlement_type,
} = invoice
const { business_activity: ba } = pos.complex
const unknown_customer = (invoice.unknown_customer || {}) as Record<string, string>
const { partner } = (ba.consumer.legal || ba.consumer.individual)!
return {
id,
invoice_number,
invoice_date,
settlement_type,
total_amount: Number(total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
economic_code: ba.economic_code,
fiscal_id: ba.fiscal_id,
template: ba.guild.invoice_template,
token: ba.partner_token,
tsp_provider: partner.tsp_provider!,
last_tax_id: invoice.reference_invoice.tax_id!,
payments: invoice.payments.map(payment => ({
amount: Number(payment.amount),
payment_method: payment.payment_method,
paid_at: payment.paid_at,
terminal_info: {
card_number: payment.terminal_info?.customer_card_no
? payment.terminal_info.customer_card_no
: undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
},
})),
customer:
invoice.customer && invoice.customer.type !== CustomerType.UNKNOWN
? {
type: invoice.customer.type,
legal_info: invoice.customer.legal
? {
name: invoice.customer.legal.name ?? undefined,
registration_number:
invoice.customer.legal.registration_number ?? undefined,
postal_code: invoice.customer.legal.postal_code ?? undefined,
economic_code: invoice.customer.legal.economic_code ?? undefined,
}
: undefined,
individual_info: invoice.customer.individual
? {
first_name: invoice.customer.individual.first_name ?? undefined,
last_name: invoice.customer.individual.last_name ?? undefined,
national_id: invoice.customer.individual.national_id ?? undefined,
mobile_number: invoice.customer.individual.mobile_number ?? undefined,
postal_code: invoice.customer.individual.postal_code ?? undefined,
economic_code: invoice.customer.individual.economic_code ?? undefined,
}
: undefined,
}
: {
type: CustomerType.UNKNOWN,
unknown_info: {
name: unknown_customer?.name || '',
economic_code: unknown_customer?.economic_code || '',
},
},
items: invoice.items.map(item => ({
invoice_item_id: item.id,
quantity: Number(item.quantity),
unit_price: Number(item.unit_price),
total_amount: Number(item.total_amount),
tax_amount: Number(tax_amount),
discount_amount: Number(discount_amount),
measure_unit: item.measure_unit_code,
sku: item.sku_code,
sku_vat: String(item.sku_vat),
discount: String((item.payload as Record<string, any>)?.discount || '0'),
good_id: item.good_id,
service_id: item.service_id,
good_snapshot: item.good_snapshot,
gold_type_payload: isGoldTypePayload(item.payload) ? item.payload : undefined,
})),
}
}
export async function buildOriginalPayload( export async function buildOriginalPayload(
prisma: PrismaService, prisma: PrismaService,
invoiceId: string, invoiceId: string,
@@ -419,6 +633,9 @@ export async function buildOriginalPayload(
select: { select: {
stan: true, stan: true,
rrn: true, rrn: true,
transaction_date_time: true,
customer_card_no: true,
terminal_id: true,
}, },
}, },
}, },
@@ -465,7 +682,9 @@ export async function buildOriginalPayload(
payment_method: payment.payment_method, payment_method: payment.payment_method,
paid_at: payment.paid_at, paid_at: payment.paid_at,
terminal_info: { terminal_info: {
card_number: payment.terminal_info ? payment.terminal_info.stan : undefined, card_number: payment.terminal_info?.customer_card_no
? payment.terminal_info.customer_card_no
: undefined,
tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined, tracking_code: payment.terminal_info ? payment.terminal_info.rrn : undefined,
}, },
})), })),
@@ -653,6 +872,8 @@ export async function onResult(
data: attemptUpdatedData, data: attemptUpdatedData,
}) })
console.log('attemptUpdatedData', attemptUpdatedData)
const updatedInvoice = await tx.salesInvoice.update({ const updatedInvoice = await tx.salesInvoice.update({
where: { id: invoice_id }, where: { id: invoice_id },
data: { data: {
+2 -10
View File
@@ -1,21 +1,13 @@
import { prismaAdapter } from '@/lib/prisma'
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common' import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
import { PrismaMariaDb } from '@prisma/adapter-mariadb'
import 'dotenv/config' import 'dotenv/config'
import { env } from 'prisma/config'
import { PrismaClient } from '../generated/prisma/client' import { PrismaClient } from '../generated/prisma/client'
import { getPrismaOptions } from './prisma-config.service' import { getPrismaOptions } from './prisma-config.service'
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() { constructor() {
const adapter = new PrismaMariaDb({ const adapter = prismaAdapter
host: env('DATABASE_HOST'),
user: env('DATABASE_USER'),
password: env('DATABASE_PASSWORD'),
database: env('DATABASE_NAME'),
port: env('DATABASE_PORT') ? Number(env('DATABASE_PORT')) : 3306,
connectionLimit: 10,
})
const prismaOptions = getPrismaOptions() const prismaOptions = getPrismaOptions()
super({ ...prismaOptions, adapter }) super({ ...prismaOptions, adapter })
+11
View File
@@ -252,6 +252,17 @@ export class RedisService implements OnModuleDestroy {
} }
async onModuleDestroy() { async onModuleDestroy() {
if (this.client.status === 'end') {
return
}
try {
await this.client.quit() await this.client.quit()
} catch (error) {
const message = (error as Error)?.message || ''
if (!message.includes('Connection is closed')) {
this.logger.warn(`Redis shutdown failed: ${message}`)
}
}
} }
} }