diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9231e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +node_modules +npm-debug.log +dist +.env +.env.local +.env.*.local +.git +.gitignore +README.md +.vscode +.idea +.DS_Store +*.log +coverage +.nyc_output +.jest-cache +prisma/dev.db +prisma/dev.db-journal +.turbo +.next +out +build +dist diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ea31974 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Server Configuration +NODE_ENV=development +PORT=5002 + +# Database Configuration +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=consumer_db +DB_USER=consumer_user +DB_PASSWORD=consumer_password +DB_ROOT_PASSWORD=root_password +DATABASE_URL=mysql://consumer_user:consumer_password@localhost:3306/consumer_db +SHADOW_DATABASE_URL=mysql://consumer_user:consumer_password@localhost:3306/consumer_db_shadow + +# JWT Configuration +JWT_SECRET=your_super_secret_jwt_key_change_this_in_production +JWT_EXPIRATION=604800 + +# CORS Configuration +CORS_ORIGINS=http://localhost:5000,http://127.0.0.1:5000 + +# File Upload Configuration +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8445115 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy dependency files +COPY pnpm-lock.yaml package.json ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Generate Prisma client and build the application +RUN pnpm exec prisma generate +RUN pnpm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install only production dependencies +RUN pnpm install --frozen-lockfile --prod + +# Copy built application from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY prisma ./prisma + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs + +# Expose port +EXPOSE 5002 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:5002/api/v1/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" + +# Use dumb-init to handle signals properly +ENTRYPOINT ["dumb-init", "--"] + +# Start application +CMD ["node", "dist/main.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a1a9125 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +version: "3.9" + +services: + database: + image: mariadb:11.4-jammy + container_name: consumer_api_db + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root_password} + MARIADB_DATABASE: ${DB_NAME:-consumer_db} + MARIADB_USER: ${DB_USER:-consumer_user} + MARIADB_PASSWORD: ${DB_PASSWORD:-consumer_password} + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: "false" + ports: + - "${DB_PORT:-3306}:3306" + volumes: + - mariadb_data:/var/lib/mysql + - ./prisma/migrations:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - consumer_network + + api: + build: + context: . + dockerfile: Dockerfile + container_name: consumer_api + restart: unless-stopped + depends_on: + database: + condition: service_healthy + environment: + NODE_ENV: ${NODE_ENV:-production} + PORT: ${PORT:-5002} + DATABASE_URL: mysql://${DB_USER:-consumer_user}:${DB_PASSWORD:-consumer_password}@database:3306/${DB_NAME:-consumer_db} + SHADOW_DATABASE_URL: mysql://${DB_USER:-consumer_user}:${DB_PASSWORD:-consumer_password}@database:3306/${DB_NAME:-consumer_db}_shadow + JWT_SECRET: ${JWT_SECRET:-your_jwt_secret_key_change_in_production} + JWT_EXPIRATION: ${JWT_EXPIRATION:-604800} + CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5000,http://127.0.0.1:5000} + ports: + - "${PORT:-5002}:5002" + volumes: + - ./src:/app/src + - /app/dist + - /app/node_modules + networks: + - consumer_network + healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://localhost:5002/api/v1/health", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +volumes: + mariadb_data: + driver: local + +networks: + consumer_network: + driver: bridge diff --git a/src/common/guards/pos.gaurd.ts b/src/common/guards/pos.gaurd.ts index 2bcb924..20735f2 100644 --- a/src/common/guards/pos.gaurd.ts +++ b/src/common/guards/pos.gaurd.ts @@ -60,7 +60,7 @@ export class PosGuard implements CanActivate { user: { accounts: { some: { - account_id: account.account_id, + id: account.account_id, }, }, }, @@ -81,20 +81,16 @@ export class PosGuard implements CanActivate { throw new ForbiddenException('شما دسترسی لازم را ندارید.') } - const foundedAccount = await this.prisma.account.findUnique({ + const foundedAccount = await this.prisma.consumerAccount.findUnique({ where: { id: account.account_id, }, select: { - consumer_account: { - select: { - role: true, - }, - }, + role: true, }, }) - if (foundedAccount?.consumer_account?.role === 'OWNER') { + if (foundedAccount?.role === 'OWNER') { return true } diff --git a/src/modules/auth/auth.utils.ts b/src/modules/auth/auth.utils.ts index 0084e45..11ee5ed 100644 --- a/src/modules/auth/auth.utils.ts +++ b/src/modules/auth/auth.utils.ts @@ -40,7 +40,6 @@ export class AuthUtils { id: accountId, }, select: { - id: true, admin_account: { select: { id: true, @@ -58,10 +57,16 @@ export class AuthUtils { }, }) - if (!adminAccount) throw new NotFoundException('متاسفانه کاربر شما یافت نشد') + if (!adminAccount || !adminAccount.admin_account) + throw new NotFoundException('متاسفانه کاربر شما یافت نشد') + + const { user, id } = adminAccount.admin_account return { - ...adminAccount.admin_account?.user, - fullname: `${adminAccount.admin_account?.user.first_name} ${adminAccount.admin_account?.user.last_name}`, + account_id: id, + user: { + ...user, + fullname: `${user.first_name} ${user.last_name}`, + }, } } @@ -73,6 +78,7 @@ export class AuthUtils { select: { consumer_account: { select: { + id: true, user: { select: { id: true, @@ -86,8 +92,17 @@ export class AuthUtils { }, }) - if (!consumerAccount) throw new NotFoundException('متاسفانه کاربر شما یافت نشد') - return consumerAccount.consumer_account?.user + if (!consumerAccount || !consumerAccount.consumer_account) + throw new NotFoundException('متاسفانه کاربر شما یافت نشد') + + const { user, id } = consumerAccount.consumer_account + return { + account_id: id, + user: { + ...user, + fullname: `${user.first_name} ${user.last_name}`, + }, + } } async getProviderAccountInfo(accountId: string) { @@ -98,6 +113,7 @@ export class AuthUtils { select: { provider_account: { select: { + id: true, user: { select: { id: true, @@ -111,8 +127,17 @@ export class AuthUtils { }, }) - if (!providerAccount) throw new NotFoundException('متاسفانه کاربر شما یافت نشد') - return providerAccount.provider_account?.user + if (!providerAccount || !providerAccount.provider_account) + throw new NotFoundException('متاسفانه کاربر شما یافت نشد') + + const { user, id } = providerAccount.provider_account + return { + account_id: id, + user: { + ...user, + fullname: user.name, + }, + } } async getPartnerAccountInfo(accountId: string) { @@ -123,6 +148,7 @@ export class AuthUtils { select: { partner_account: { select: { + id: true, user: { select: { id: true, @@ -136,33 +162,50 @@ export class AuthUtils { }, }) - if (!partnerAccount) throw new NotFoundException('متاسفانه کاربر شما یافت نشد') - return partnerAccount.partner_account?.user + if (!partnerAccount || !partnerAccount.partner_account) + throw new NotFoundException('متاسفانه کاربر شما یافت نشد') + + const { user, id } = partnerAccount.partner_account + return { + account_id: id, + user: { + ...user, + fullname: user.name, + }, + } } async generateLoginResponse(account: Account) { let preparedAccount = { type: account.type, username: account.username, - account_id: account.id, + account_id: '', user: {}, } as AccessTokenPayload + let accountInfo = { + user: {}, + account_id: '', + } + switch (account.type) { case AccountType.ADMIN: - preparedAccount.user = await this.getAdminAccountInfo(account.id) + accountInfo = await this.getAdminAccountInfo(account.id) break case AccountType.CONSUMER: - preparedAccount.user = await this.getConsumerAccountInfo(account.id) + accountInfo = await this.getConsumerAccountInfo(account.id) break case AccountType.PARTNER: - preparedAccount.user = await this.getPartnerAccountInfo(account.id) + accountInfo = await this.getPartnerAccountInfo(account.id) break case AccountType.PROVIDER: - preparedAccount.user = await this.getProviderAccountInfo(account.id) + accountInfo = await this.getProviderAccountInfo(account.id) break } + preparedAccount.account_id = accountInfo.account_id + preparedAccount.user = accountInfo.user + if (!preparedAccount.user) { throw new NotFoundException('متاسفانه کاربر شما یافت نشد') } else { diff --git a/src/modules/consumer/business-activities/complexes/poses/poses.module.ts b/src/modules/consumer/business-activities/complexes/poses/poses.module.ts index 8718b22..629d3fb 100644 --- a/src/modules/consumer/business-activities/complexes/poses/poses.module.ts +++ b/src/modules/consumer/business-activities/complexes/poses/poses.module.ts @@ -2,9 +2,10 @@ import { PrismaModule } from '@/prisma/prisma.module' import { Module } from '@nestjs/common' import { ComplexPosesController } from './poses.controller' import { ComplexPosesService } from './poses.service' +import { ConsumerPosSalesInvoicesModule } from './sales-invoices/sales-invoices.module' @Module({ - imports: [PrismaModule], + imports: [PrismaModule, ConsumerPosSalesInvoicesModule], controllers: [ComplexPosesController], providers: [ComplexPosesService], exports: [ComplexPosesService], diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/dto/create-sales-invoice.dto.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/dto/create-sales-invoice.dto.ts new file mode 100644 index 0000000..23fcb86 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/dto/create-sales-invoice.dto.ts @@ -0,0 +1,66 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger' +import { + ArrayMinSize, + IsDateString, + IsEnum, + IsNumber, + IsObject, + IsOptional, + IsString, +} from 'class-validator' +import type { CustomerIndividual, CustomerLegal } from 'generated/prisma/client' +import { CustomerType } from 'generated/prisma/enums' +import { CreateSalesInvoiceItemDto } from '../sales-invoice-items/dto/create-sales-invoice-item.dto' + +export class CreateSalesInvoiceDto { + // @TODO: totalAmount must calculated instead of get from api + @IsNumber() + @ApiProperty({ required: true, default: 0 }) + total_amount: number + + @ApiProperty({ required: true }) + @IsDateString( + { strict: true }, + { message: 'invoice_date must be a valid ISO-8601 string' }, + ) + invoice_date: Date + + @ApiProperty({ required: true }) + @IsObject() + payments: { + terminal: number + cash: number + set_off: number + } + + @ApiProperty() + @ArrayMinSize(1) + items: CreateSalesInvoiceItemDto[] + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + notes?: string + + @ApiProperty({ required: true, default: CustomerType.UNKNOWN }) + @IsEnum(CustomerType) + customer_type: CustomerType + + @IsOptional() + @IsString() + @ApiProperty({ required: false }) + customer_id?: string + + @ApiProperty({ required: false }) + @IsOptional() + @IsObject() + customer?: + | { + first_name: string + last_name: string + } + | CustomerIndividual + | CustomerLegal +} + +export class UpdateSalesInvoiceDto extends PartialType(CreateSalesInvoiceDto) {} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/dto/create-sales-invoice-item.dto.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/dto/create-sales-invoice-item.dto.ts new file mode 100644 index 0000000..6f85bec --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/dto/create-sales-invoice-item.dto.ts @@ -0,0 +1,57 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger' +import { IsEnum, IsNumber, IsObject, IsOptional, IsString } from 'class-validator' +import type { SaleInvoiceType } from 'common/interfaces/sale-invoice-payload' +import { UnitType } from 'generated/prisma/enums' + +export class CreateSalesInvoiceItemDto { + @IsNumber() + @ApiProperty({ required: true }) + unit_price: number + + @IsNumber() + @ApiProperty({ required: true, default: 1 }) + quantity: number + + @IsNumber() + @ApiProperty({ required: true }) + total_amount: number + + @IsNumber() + @ApiProperty({ required: true }) + discount_amount: number + + @IsString() + @ApiProperty({ required: true }) + invoice_id: string + + @IsEnum(UnitType) + @ApiProperty({ enum: Object.values(UnitType) }) + unit_type: UnitType + + @IsOptional() + @IsString() + @ApiProperty({ required: false }) + good_id?: string + + @IsOptional() + @IsString() + @ApiProperty({ required: false }) + service_id?: string + + // @IsEnum(SalesInvoiceItemPricingModel) + // @ApiProperty({ enum: Object.values(SalesInvoiceItemPricingModel) }) + // @IsOptional() + // pricingModel: SalesInvoiceItemPricingModel + + @IsOptional() + @IsObject() + @ApiProperty({ required: false, default: {} }) + payload: SaleInvoiceType + + @IsOptional() + @IsString() + @ApiProperty({ required: false, default: '' }) + notes: string +} + +export class UpdateSalesInvoiceItemDto extends PartialType(CreateSalesInvoiceItemDto) {} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.controller.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.controller.ts new file mode 100644 index 0000000..9efb0f8 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.controller.ts @@ -0,0 +1,22 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common' +import { SalesInvoiceItemsService } from './sales-invoice-items.service' + +@Controller('sales_invoice_items') +export class SalesInvoiceItemsController { + constructor(private readonly salesInvoiceItemsService: SalesInvoiceItemsService) {} + + @Get() + findAll() { + return this.salesInvoiceItemsService.findAll() + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.salesInvoiceItemsService.findOne(+id) + } + + @Post() + create(@Body() data: any) { + return this.salesInvoiceItemsService.create(data) + } +} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.module.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.module.ts new file mode 100644 index 0000000..f444199 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { SalesInvoiceItemsController } from './sales-invoice-items.controller' +import { SalesInvoiceItemsService } from './sales-invoice-items.service' + +@Module({ + controllers: [SalesInvoiceItemsController], + providers: [SalesInvoiceItemsService], +}) +export class SalesInvoiceItemsModule {} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.service.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.service.ts new file mode 100644 index 0000000..45655b4 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-items/sales-invoice-items.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common' + +@Injectable() +export class SalesInvoiceItemsService { + findAll() { + // TODO: Implement fetching all sales invoice items + return [] + } + + findOne(id: number) { + // TODO: Implement fetching a single sales invoice item + return {} + } + + create(data: any) { + // TODO: Implement sales invoice item creation + return data + } +} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/dto/create-sales-invoice-payment.dto.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/dto/create-sales-invoice-payment.dto.ts new file mode 100644 index 0000000..71b7549 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/dto/create-sales-invoice-payment.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty, PartialType } from '@nestjs/swagger' +import { IsDateString, IsEnum, IsNumber, IsString } from 'class-validator' + +enum PaymentMethodType { + CASH = 'CASH', + CARD = 'CARD', + BANK = 'BANK', + CHECK = 'CHECK', + OTHER = 'OTHER', +} + +export class CreateSalesInvoicePaymentDto { + @ApiProperty() + @IsString() + invoice_id: string + + @ApiProperty() + @IsNumber() + amount: number + + @ApiProperty({ enum: PaymentMethodType }) + @IsEnum(PaymentMethodType) + payment_method: PaymentMethodType + + @ApiProperty() + @IsDateString() + paid_at: string +} + +export class UpdateSalesInvoicePaymentDto extends PartialType( + CreateSalesInvoicePaymentDto, +) {} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.controller.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.controller.ts new file mode 100644 index 0000000..ad4d9e8 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.controller.ts @@ -0,0 +1,24 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common' +import { SalesInvoicePaymentsService } from './sales-invoice-payments.service' + +@Controller('sales-invoice-payments') +export class SalesInvoicePaymentsController { + constructor( + private readonly salesInvoicePaymentsService: SalesInvoicePaymentsService, + ) {} + + @Get() + findAll() { + return this.salesInvoicePaymentsService.findAll() + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.salesInvoicePaymentsService.findOne(+id) + } + + @Post() + create(@Body() data: any) { + return this.salesInvoicePaymentsService.create(data) + } +} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.module.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.module.ts new file mode 100644 index 0000000..35803e1 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { SalesInvoicePaymentsController } from './sales-invoice-payments.controller' +import { SalesInvoicePaymentsService } from './sales-invoice-payments.service' + +@Module({ + controllers: [SalesInvoicePaymentsController], + providers: [SalesInvoicePaymentsService], +}) +export class SalesInvoicePaymentsModule {} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.service.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.service.ts new file mode 100644 index 0000000..3f783f7 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoice-payments/sales-invoice-payments.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common' + +@Injectable() +export class SalesInvoicePaymentsService { + findAll() { + // TODO: Implement fetching all sales invoice payments + return [] + } + + findOne(id: number) { + // TODO: Implement fetching a single sales invoice payment + return {} + } + + create(data: any) { + // TODO: Implement sales invoice payment creation + return data + } +} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.controller.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.controller.ts new file mode 100644 index 0000000..daed5de --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get, Param } from '@nestjs/common' + +import { TokenAccount } from '@/common/decorators/tokenInfo.decorator' +import { ApiTags } from '@nestjs/swagger' +import { SalesInvoicesService } from './sales-invoices.service' + +@ApiTags('ConsumerComplexPoses') +@Controller('consumer/complexes/:complexId/poses/:posId/sales_invoices') +export class SalesInvoicesController { + constructor(private readonly salesInvoicesService: SalesInvoicesService) {} + + @Get() + findAll( + @TokenAccount('userId') userId: string, + @Param('complexId') complexId: string, + @Param('posId') posId: string, + ) { + return this.salesInvoicesService.findAll(userId, complexId, posId) + } + + @Get(':id') + findOne( + @Param('complexId') complexId: string, + @Param('posId') posId: string, + @Param('id') id: string, + ) { + return this.salesInvoicesService.findOne(complexId, posId, id) + } +} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.module.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.module.ts new file mode 100644 index 0000000..ad47e25 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common' +import { SalesInvoicesController } from './sales-invoices.controller' +import { SalesInvoicesService } from './sales-invoices.service' + +@Module({ + controllers: [SalesInvoicesController], + providers: [SalesInvoicesService], +}) +export class ConsumerPosSalesInvoicesModule {} diff --git a/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.service.ts b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.service.ts new file mode 100644 index 0000000..a8bc2b3 --- /dev/null +++ b/src/modules/consumer/business-activities/complexes/poses/sales-invoices/sales-invoices.service.ts @@ -0,0 +1,108 @@ +import { ResponseMapper } from '@/common/response/response-mapper' +import { SalesInvoiceWhereInput } from '@/generated/prisma/models' +import { PrismaService } from '@/prisma/prisma.service' +import { Injectable } from '@nestjs/common' + +@Injectable() +export class SalesInvoicesService { + constructor(private prisma: PrismaService) {} + + async findAll(user_id: string, complex_id: string, pos_id: string) { + const defaultWhere: SalesInvoiceWhereInput = { + pos_id, + pos: { + complex: { + id: complex_id, + business_activity: { + user_id, + }, + }, + }, + } + + const pageSize = 10 + const page = 1 + + const [items, count] = await this.prisma.$transaction([ + this.prisma.salesInvoice.findMany({ + where: defaultWhere, + select: { + id: true, + code: true, + invoice_date: true, + notes: true, + total_amount: true, + + items: { + select: { + unit_type: true, + discount: true, + notes: true, + quantity: true, + total_amount: true, + unit_price: true, + payload: true, + good: { + select: { + id: true, + name: true, + sku: true, + barcode: true, + local_sku: true, + pricing_model: true, + unit_type: true, + category: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }, + payments: { + select: { + amount: true, + paid_at: true, + payment_method: true, + }, + }, + customer: { + select: { + type: true, + customer_individuals: { + select: { + economic_code: true, + first_name: true, + last_name: true, + postal_code: true, + national_id: true, + }, + }, + customer_legals: { + select: { + economic_code: true, + postal_code: true, + registration_number: true, + }, + }, + }, + }, + unknown_customer: true, + }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.salesInvoice.count({ + where: defaultWhere, + }), + ]) + return ResponseMapper.paginate(items, { count, page, pageSize }) + } + + findOne(complex_id: string, pos_id: string, id: string) { + // TODO: Implement fetching a single sales invoice + return {} + } +} diff --git a/src/modules/pos/pos.middleware.ts b/src/modules/pos/pos.middleware.ts index 8e7a07e..8c5ae9c 100644 --- a/src/modules/pos/pos.middleware.ts +++ b/src/modules/pos/pos.middleware.ts @@ -27,7 +27,7 @@ export class PosMiddleware implements NestMiddleware { const consumerAccount = await this.prisma.consumerAccount.findUnique({ where: { - account_id: tokenAccount.account_id, + id: tokenAccount.account_id, }, }) if (!consumerAccount) { diff --git a/src/modules/pos/sales-invoices/dto/create-sales-invoice.dto.ts b/src/modules/pos/sales-invoices/dto/create-sales-invoice.dto.ts index 23fcb86..4ccc3ef 100644 --- a/src/modules/pos/sales-invoices/dto/create-sales-invoice.dto.ts +++ b/src/modules/pos/sales-invoices/dto/create-sales-invoice.dto.ts @@ -54,13 +54,14 @@ export class CreateSalesInvoiceDto { @ApiProperty({ required: false }) @IsOptional() @IsObject() - customer?: - | { - first_name: string - last_name: string - } - | CustomerIndividual - | CustomerLegal + customer?: { + customer_individual?: Omit + customer_legal?: Omit + customer_unknown?: { + first_name: string + last_name: string + } + } } export class UpdateSalesInvoiceDto extends PartialType(CreateSalesInvoiceDto) {} diff --git a/src/modules/pos/sales-invoices/sales-invoices.service.ts b/src/modules/pos/sales-invoices/sales-invoices.service.ts index 8afc8a4..5e5b743 100644 --- a/src/modules/pos/sales-invoices/sales-invoices.service.ts +++ b/src/modules/pos/sales-invoices/sales-invoices.service.ts @@ -1,6 +1,7 @@ import { IPosPayload } from '@/common/models/posPayload.model' +import { SalesInvoiceCreateInput } from '@/generated/prisma/models' import { PrismaService } from '@/prisma/prisma.service' -import { Injectable } from '@nestjs/common' +import { BadRequestException, Injectable } from '@nestjs/common' import { ResponseMapper } from 'common/response/response-mapper' import type { CustomerIndividual, CustomerLegal } from 'generated/prisma/client' import { CustomerType, PaymentMethodType } from 'generated/prisma/enums' @@ -65,138 +66,128 @@ export class SalesInvoicesService { if (customer_id) { } else if ( customer_type === CustomerType.INDIVIDUAL && - isCustomerIndividual(customer) + customer?.customer_individual ) { - let customerIndividual = await $tx.customerIndividual.findUnique({ + const customer_individual = customer.customer_individual + const customerIndividual = await $tx.customerIndividual.upsert({ where: { complex_id_national_id: { complex_id, - national_id: customer.national_id, + national_id: customer_individual.national_id, }, }, + create: { + ...customer_individual, + customer: { + create: { + type: CustomerType.INDIVIDUAL, + }, + }, + complex: { + connect: { + id: complex_id, + }, + }, + }, + update: {}, + select: { + customer_id: true, + }, }) - if (customerIndividual) { - await $tx.customerIndividual.update({ - where: { - complex_id_national_id: { - complex_id, - national_id: customer.national_id, - }, - }, - data: { - ...customer, - }, - }) - } else { - const customerIndividual = await $tx.customerIndividual.create({ - data: { - ...customer, - }, - }) - const createdCustomer = await $tx.customer.create({ - data: { - type: CustomerType.INDIVIDUAL, - customer_individuals: { - connect: { - customer_id: customerIndividual.customer_id, - }, - }, - }, - }) - - if (createdCustomer && createdCustomer.id) { - newCustomerId = createdCustomer.id - } + if (!customerIndividual) { + throw new BadRequestException('متاسفانه مشکلی در اطلاعات مشتری وجود دارد.') } - } else if (data.customer_type === CustomerType.LEGAL && isCustomerLegal(customer)) { - let customerLegal = await $tx.customerLegal.findUnique({ + newCustomerId = customerIndividual.customer_id + } else if (data.customer_type === CustomerType.LEGAL && customer?.customer_legal) { + const customer_legal = customer.customer_legal + + const customerLegal = await $tx.customerLegal.upsert({ where: { complex_id_registration_number: { complex_id, - registration_number: customer.registration_number, + registration_number: customer_legal.registration_number, }, }, + create: { + ...customer_legal, + customer: { + create: { + type: CustomerType.LEGAL, + }, + }, + complex: { + connect: { + id: complex_id, + }, + }, + }, + update: {}, + select: { + customer_id: true, + }, }) - if (customerLegal) { - await $tx.customerLegal.update({ - where: { - complex_id_registration_number: { - complex_id, - registration_number: customer.registration_number, - }, - }, - data: { - ...customer, - }, - }) - } else { - const customerLegal = await $tx.customerLegal.create({ - data: { - ...customer, - }, - }) - const createdCustomer = await $tx.customer.create({ - data: { - type: CustomerType.LEGAL, - - customer_individuals: { - connect: { - customer_id: customerLegal.customer_id, - }, - }, - }, - }) - - if (createdCustomer && createdCustomer.id) { - newCustomerId = createdCustomer.id - } + if (!customerLegal) { + throw new BadRequestException('متاسفانه مشکلی در اطلاعات مشتری وجود دارد.') } + newCustomerId = customerLegal.customer_id } else if (data.customer_type === CustomerType.UNKNOWN) { } - const salesInvoice = await $tx.salesInvoice.create({ - data: { - ...invoiceData, - total_amount: data.total_amount, - code: 'INV-' + Date.now(), + const salesInvoiceData: SalesInvoiceCreateInput = { + ...invoiceData, + total_amount: data.total_amount, + code: 'INV-' + Date.now(), - items: { - createMany: { - data: data.items.map(item => ({ - good_id: item.good_id, - quantity: item.quantity, - unit_price: item.unit_price, - total_amount: item.total_amount, - payload: JSON.parse(JSON.stringify(item.payload)), - unit_type: item.unit_type, - // pricing_model: item.pricingModel, - })), - }, - }, - unknown_customer: {}, - payments: { - createMany: { - data: payments, - }, - }, - // customer: { - // connect: { - // id: newCustomerId, - // }, - // }, - account: { - connect: { - id: consumer_account_id, - }, - }, - pos: { - connect: { - id: pos_id, - }, + items: { + createMany: { + data: data.items.map(item => ({ + good_id: item.good_id, + quantity: item.quantity, + unit_price: item.unit_price, + total_amount: item.total_amount, + payload: item.payload + ? JSON.parse(JSON.stringify(item.payload)) + : undefined, + unit_type: item.unit_type, + // pricing_model: item.pricingModel, + })), }, }, + unknown_customer: {}, + payments: { + createMany: { + data: payments, + }, + }, + // customer: { + // connect: { + // id: newCustomerId || undefined, + // }, + // }, + account: { + connect: { + id: consumer_account_id, + }, + }, + pos: { + connect: { + id: pos_id, + }, + }, + } + + if (newCustomerId) { + salesInvoiceData.customer = { + connect: { + id: newCustomerId || undefined, + }, + } + } + + const salesInvoice = await $tx.salesInvoice.create({ + data: salesInvoiceData, }) return salesInvoice