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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions backend/src/invoices/dto/create-invoice.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
IsString,
IsNumber,
IsArray,
IsOptional,
IsDateString,
} from 'class-validator';

export class CreateInvoiceDto {
@IsString()
userId: string;

@IsString()
orderId: string;

@IsString()
paymentId: string;

@IsDateString()
issuedAt: Date;

@IsOptional()
@IsDateString()
dueAt?: Date;

@IsArray()
items: Record<string, any>[];

@IsNumber()
subtotal: number;

@IsNumber()
tax: number;

@IsNumber()
total: number;

@IsString()
currency: string;

@IsOptional()
@IsString()
notes?: string;
}
16 changes: 16 additions & 0 deletions backend/src/invoices/dto/filter-invoice.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IsOptional, IsEnum, IsDateString } from 'class-validator';
import { InvoiceStatus } from '../enums/invoice-status.enum';

export class FilterInvoiceDto {
@IsOptional()
@IsEnum(InvoiceStatus)
status?: InvoiceStatus;

@IsOptional()
@IsDateString()
startDate?: Date;

@IsOptional()
@IsDateString()
endDate?: Date;
}
59 changes: 59 additions & 0 deletions backend/src/invoices/entities/invoice.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
} from 'typeorm';
import { InvoiceStatus } from '../enums/invoice-status.enum';

@Entity()
export class Invoice {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
invoiceNumber: string;

@Column()
userId: string;

@Column()
orderId: string;

@Column()
paymentId: string;

@Column({
type: 'enum',
enum: InvoiceStatus,
default: InvoiceStatus.DRAFT,
})
status: InvoiceStatus;

@Column({ type: 'timestamp' })
issuedAt: Date;

@Column({ type: 'timestamp', nullable: true })
dueAt: Date;

@Column({ type: 'jsonb' })
items: Record<string, any>[];

@Column('decimal')
subtotal: number;

@Column('decimal')
tax: number;

@Column('decimal')
total: number;

@Column()
currency: string;

@Column({ nullable: true })
notes: string;

@CreateDateColumn()
createdAt: Date;
}
6 changes: 6 additions & 0 deletions backend/src/invoices/enums/invoice-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum InvoiceStatus {
DRAFT = 'draft',
SENT = 'sent',
PAID = 'paid',
VOID = 'void',
}
57 changes: 57 additions & 0 deletions backend/src/invoices/invoices.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Controller,
Post,
Get,
Param,
Body,
Query,
} from '@nestjs/common';

import { InvoicesService } from './invoices.service';
import { CreateInvoiceDto } from './dto/create-invoice.dto';
import { FilterInvoiceDto } from './dto/filter-invoice.dto';

@Controller('invoices')
export class InvoicesController {
constructor(
private readonly invoicesService: InvoicesService,
) {}

@Post()
create(@Body() dto: CreateInvoiceDto) {
return this.invoicesService.createInvoice(dto);
}

@Get()
getInvoices(
@Query('page') page = 1,
@Query('limit') limit = 10,
@Query() filter: FilterInvoiceDto,
) {
return this.invoicesService.getInvoices(
Number(page),
Number(limit),
filter,
);
}

@Get(':id')
getInvoice(@Param('id') id: string) {
return this.invoicesService.getInvoice(id);
}

@Get(':id/download')
downloadInvoice(@Param('id') id: string) {
return this.invoicesService.downloadInvoice(id);
}

@Post(':id/send')
sendInvoice(@Param('id') id: string) {
return this.invoicesService.sendInvoice(id);
}

@Post(':id/void')
voidInvoice(@Param('id') id: string) {
return this.invoicesService.voidInvoice(id);
}
}
14 changes: 14 additions & 0 deletions backend/src/invoices/invoices.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { InvoicesController } from './invoices.controller';
import { InvoicesService } from './invoices.service';
import { Invoice } from './entities/invoice.entity';

@Module({
imports: [TypeOrmModule.forFeature([Invoice])],
controllers: [InvoicesController],
providers: [InvoicesService],
exports: [InvoicesService],
})
export class InvoicesModule {}
133 changes: 133 additions & 0 deletions backend/src/invoices/invoices.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';

import { Invoice } from './entities/invoice.entity';
import { CreateInvoiceDto } from './dto/create-invoice.dto';
import { FilterInvoiceDto } from './dto/filter-invoice.dto';
import { InvoiceStatus } from './enums/invoice-status.enum';

@Injectable()
export class InvoicesService {
constructor(
@InjectRepository(Invoice)
private invoiceRepo: Repository<Invoice>,
) {}

private async generateInvoiceNumber(): Promise<string> {
const now = new Date();
const yearMonth = `${now.getFullYear()}${(
now.getMonth() + 1
)
.toString()
.padStart(2, '0')}`;

const count = await this.invoiceRepo.count();

const sequence = (count + 1)
.toString()
.padStart(4, '0');

return `INV-${yearMonth}-${sequence}`;
}

async createInvoice(dto: CreateInvoiceDto): Promise<Invoice> {
const invoiceNumber = await this.generateInvoiceNumber();

const invoice = this.invoiceRepo.create({
...dto,
invoiceNumber,
status: InvoiceStatus.DRAFT,
});

return this.invoiceRepo.save(invoice);
}

async getInvoices(
page = 1,
limit = 10,
filter?: FilterInvoiceDto,
) {
const where: any = {};

if (filter?.status) {
where.status = filter.status;
}

if (filter?.startDate && filter?.endDate) {
where.issuedAt = Between(
filter.startDate,
filter.endDate,
);
}

const [data, total] = await this.invoiceRepo.findAndCount({
where,
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});

return {
data,
total,
page,
limit,
};
}

async getInvoice(id: string): Promise<Invoice> {
const invoice = await this.invoiceRepo.findOne({
where: { id },
});

if (!invoice) {
throw new NotFoundException('Invoice not found');
}

return invoice;
}

async downloadInvoice(id: string) {
const invoice = await this.getInvoice(id);

return {
invoiceNumber: invoice.invoiceNumber,
issuedAt: invoice.issuedAt,
dueAt: invoice.dueAt,
items: invoice.items,
subtotal: invoice.subtotal,
tax: invoice.tax,
total: invoice.total,
currency: invoice.currency,
notes: invoice.notes,
userId: invoice.userId,
orderId: invoice.orderId,
paymentId: invoice.paymentId,
};
}

async sendInvoice(id: string) {
const invoice = await this.getInvoice(id);

invoice.status = InvoiceStatus.SENT;

await this.invoiceRepo.save(invoice);

return {
message: 'Invoice marked as sent',
invoice,
};
}

async voidInvoice(id: string) {
const invoice = await this.getInvoice(id);

invoice.status = InvoiceStatus.VOID;

return this.invoiceRepo.save(invoice);
}
}
26 changes: 26 additions & 0 deletions backend/src/payment/dto/create-payment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { PaymentProvider } from '../enums/payment-provider.enum';
import { IsString, IsNumber, IsEnum, IsOptional } from 'class-validator';

export class CreatePaymentDto {
@IsString()
orderId: string;

@IsString()
userId: string;

@IsNumber()
amount: number;

@IsString()
currency: string;

@IsEnum(PaymentProvider)
provider: PaymentProvider;

@IsOptional()
@IsString()
providerReference?: string;

@IsOptional()
metadata?: Record<string, any>;
}
6 changes: 6 additions & 0 deletions backend/src/payment/dto/refund-payment.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';

export class RefundPaymentDto {
@IsString()
reason: string;
}
Loading
Loading