Skip to content

Commit

Permalink
Merge pull request #57 from ordero-team/fix-54
Browse files Browse the repository at this point in the history
[FIX-54] Owner/Cashier - Print Order Bill
  • Loading branch information
akbarsaputrait authored Sep 19, 2024
2 parents 83ee69c + 21da30a commit bc6becb
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 7 deletions.
88 changes: 86 additions & 2 deletions src/app/owner/restaurant/order/detail.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ import { Rest } from '@core/decorators/restaurant.decorator';
import { Me } from '@core/decorators/user.decorator';
import { OwnerAuthGuard } from '@core/guards/auth.guard';
import { OwnerGuard } from '@core/guards/owner.guard';
import { PdfService } from '@core/services/pdf.service';
import { PermAct, PermOwner } from '@core/services/role.service';
import { Notification, NotificationType } from '@db/entities/core/notification.entity';
import { OrderProduct, OrderProductStatus } from '@db/entities/core/order-product.entity';
import { Order, OrderStatus } from '@db/entities/core/order.entity';
import { Owner } from '@db/entities/owner/owner.entity';
import { ProductStock } from '@db/entities/owner/product-stock.entity';
import { Restaurant } from '@db/entities/owner/restaurant.entity';
import { Table, TableStatus } from '@db/entities/owner/table.entity';
import { OrderTransformer } from '@db/transformers/order.transformer';
import { GenericException } from '@lib/exceptions/generic.exception';
import { ValidationException } from '@lib/exceptions/validation.exception';
import { config } from '@lib/helpers/config.helper';
import { time } from '@lib/helpers/time.helper';
import { titleCase } from '@lib/helpers/utils.helper';
import { titleCase, writeFile } from '@lib/helpers/utils.helper';
import { Validator } from '@lib/helpers/validator.helper';
import Socket from '@lib/pubsub/pubsub.lib';
import Logger from '@lib/logger/logger.library';
import Socket, { PubSubEventType, PubSubPayloadType, PubSubStatus } from '@lib/pubsub/pubsub.lib';
import { Permissions } from '@lib/rbac';
import AppDataSource from '@lib/typeorm/datasource.typeorm';
import { uuid } from '@lib/uid/uuid.library';
Expand All @@ -25,6 +29,8 @@ import { In } from 'typeorm';
@Controller(':order_id')
@UseGuards(OwnerAuthGuard())
export class DetailController {
constructor(private pdf: PdfService) {}

static async action(order: Order, action: OrderStatus, actor: Owner) {
try {
// @TODO: How "REJECTED" flow works
Expand Down Expand Up @@ -196,4 +202,82 @@ export class DetailController {

return response.item(order, OrderTransformer);
}

@Get('/print')
@UseGuards(OwnerGuard)
@Permissions(`${PermOwner.Order}@${PermAct.R}`)
async generateBill(@Param() param, @Res() response, @Me() me: Owner, @Rest() restaurant: Restaurant) {
const request_id = uuid();
const processing = async (): Promise<string> => {
const order = await Order.findOneByOrFail({ id: param.order_id });

if (order.status !== OrderStatus.Completed) {
throw new GenericException(`Only completed order can be printed`);
}

const location = await order.location;
const staff = await order.staff;
const owner = await order.owner;

const products: { name: string; qty: string; price: string; total: string }[] = [];
const items = await order.order_products;
for (const item of items) {
const productVar = await item.product_variant;
const product = await productVar.product;
products.push({
name: product.name.length > 20 ? product.name.substring(0, 17) + '...' : product.name,
qty: item.qty.toString(),
price: item.price.toLocaleString('id-ID', { style: 'currency', currency: 'IDR' }),
total: (item.price * item.qty).toLocaleString('id-ID', { style: 'currency', currency: 'IDR' }),
});
}

const payload = {
restaurantName: restaurant.name,
restaurantAddress: location.address || '-',
restaurantPhone: restaurant.phone || '-',
receiptNumber: order.number,
billedAt: time(order.billed_at).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
cashierName: (owner || staff).logName,
items: products,
total: order.net_total.toLocaleString('id-ID', { style: 'currency', currency: 'IDR' }),
};

const pdf = await this.pdf.billInvoice(payload);
const dir = `${config.getPublicPath()}/files`;
const filename = `${time().unix()}-order-bill.pdf`;
return writeFile(dir, filename, pdf);
};

processing()
.then((path) => {
// send event
Socket.getInstance().event(me.id, {
request_id,
status: PubSubStatus.Success,
type: PubSubEventType.OwnerGetBill,
payload: {
type: PubSubPayloadType.Download,
body: {
mime: 'text/href',
name: `${time().unix()}-order-bill.pdf`,
content: config.getDownloadURI(path),
},
},
});
})
.catch(async (error) => {
// send event
Socket.getInstance().event(me.id, {
request_id,
status: PubSubStatus.Fail,
type: PubSubEventType.OwnerGetBill,
error: error.message,
});

Logger.getInstance().notify(error);
});

return response.data({ request_id });
}
}
3 changes: 3 additions & 0 deletions src/app/owner/restaurant/order/order.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Rest } from '@core/decorators/restaurant.decorator';
import { Me } from '@core/decorators/user.decorator';
import { OwnerAuthGuard } from '@core/guards/auth.guard';
import { OwnerGuard } from '@core/guards/owner.guard';
import { PdfService } from '@core/services/pdf.service';
import { PermAct, PermOwner } from '@core/services/role.service';
import { Order } from '@db/entities/core/order.entity';
import { Location } from '@db/entities/owner/location.entity';
Expand All @@ -25,6 +26,8 @@ import * as ExcelJS from 'exceljs';
@Controller()
@UseGuards(OwnerAuthGuard())
export class OrderController {
constructor(private pdf: PdfService) {}

@Get()
@UseGuards(OwnerGuard)
@Permissions(`${PermOwner.Order}@${PermAct.R}`)
Expand Down
88 changes: 86 additions & 2 deletions src/app/staff/restaurant/order/detail.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ import { Rest } from '@core/decorators/restaurant.decorator';
import { Me } from '@core/decorators/user.decorator';
import { StaffAuthGuard } from '@core/guards/auth.guard';
import { StaffGuard } from '@core/guards/staff.guard';
import { PdfService } from '@core/services/pdf.service';
import { PermAct, PermStaff } from '@core/services/role.service';
import { Notification, NotificationType } from '@db/entities/core/notification.entity';
import { OrderProduct, OrderProductStatus } from '@db/entities/core/order-product.entity';
import { Order, OrderStatus } from '@db/entities/core/order.entity';
import { ProductStock } from '@db/entities/owner/product-stock.entity';
import { Restaurant } from '@db/entities/owner/restaurant.entity';
import { Table, TableStatus } from '@db/entities/owner/table.entity';
import { StaffUser } from '@db/entities/staff/user.entity';
import { OrderTransformer } from '@db/transformers/order.transformer';
import { GenericException } from '@lib/exceptions/generic.exception';
import { ValidationException } from '@lib/exceptions/validation.exception';
import { config } from '@lib/helpers/config.helper';
import { time } from '@lib/helpers/time.helper';
import { titleCase } from '@lib/helpers/utils.helper';
import { titleCase, writeFile } from '@lib/helpers/utils.helper';
import { Validator } from '@lib/helpers/validator.helper';
import Socket from '@lib/pubsub/pubsub.lib';
import Logger from '@lib/logger/logger.library';
import Socket, { PubSubEventType, PubSubPayloadType, PubSubStatus } from '@lib/pubsub/pubsub.lib';
import { Permissions } from '@lib/rbac';
import AppDataSource from '@lib/typeorm/datasource.typeorm';
import { uuid } from '@lib/uid/uuid.library';
Expand All @@ -25,6 +29,8 @@ import { In } from 'typeorm';
@Controller(':order_id')
@UseGuards(StaffAuthGuard())
export class DetailController {
constructor(private pdf: PdfService) {}

static async action(order: Order, action: OrderStatus, actor: StaffUser) {
try {
// @TODO: How "REJECTED" flow works
Expand Down Expand Up @@ -196,4 +202,82 @@ export class DetailController {

return response.item(order, OrderTransformer);
}

@Get('/print')
@UseGuards(StaffGuard)
@Permissions(`${PermStaff.Order}@${PermAct.R}`)
async generateBill(@Param() param, @Res() response, @Me() me: StaffUser, @Rest() restaurant: Restaurant) {
const request_id = uuid();
const processing = async (): Promise<string> => {
const order = await Order.findOneByOrFail({ id: param.order_id });

if (order.status !== OrderStatus.Completed) {
throw new GenericException(`Only completed order can be printed`);
}

const location = await order.location;
const staff = await order.staff;
const owner = await order.owner;

const products: { name: string; qty: string; price: string; total: string }[] = [];
const items = await order.order_products;
for (const item of items) {
const productVar = await item.product_variant;
const product = await productVar.product;
products.push({
name: product.name.length > 20 ? product.name.substring(0, 17) + '...' : product.name,
qty: item.qty.toString(),
price: item.price.toLocaleString('id-ID', { style: 'currency', currency: 'IDR' }),
total: (item.price * item.qty).toLocaleString('id-ID', { style: 'currency', currency: 'IDR' }),
});
}

const payload = {
restaurantName: restaurant.name,
restaurantAddress: location.address || '-',
restaurantPhone: restaurant.phone || '-',
receiptNumber: order.number,
billedAt: time(order.billed_at).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
cashierName: (owner || staff).logName,
items: products,
total: order.net_total.toLocaleString('id-ID', { style: 'currency', currency: 'IDR' }),
};

const pdf = await this.pdf.billInvoice(payload);
const dir = `${config.getPublicPath()}/files`;
const filename = `${time().unix()}-order-bill.pdf`;
return writeFile(dir, filename, pdf);
};

processing()
.then((path) => {
// send event
Socket.getInstance().event(me.id, {
request_id,
status: PubSubStatus.Success,
type: PubSubEventType.StaffGetBill,
payload: {
type: PubSubPayloadType.Download,
body: {
mime: 'text/href',
name: `${time().unix()}-order-bill.pdf`,
content: config.getDownloadURI(path),
},
},
});
})
.catch(async (error) => {
// send event
Socket.getInstance().event(me.id, {
request_id,
status: PubSubStatus.Fail,
type: PubSubEventType.StaffGetBill,
error: error.message,
});

Logger.getInstance().notify(error);
});

return response.data({ request_id });
}
}
15 changes: 14 additions & 1 deletion src/core/services/pdf.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export class PdfService {
create(template: string, data: any, options?: IPdfOption) {
const html = fs.readFileSync(`${config.getTemplatePath()}/pdf/${template}.hbs`, 'utf8');
const compiled = handlebars.compile(html)(data);

const htmlToPdf = new HTMLToPDF(compiled, {
waitForNetworkIdle: options.waitForNetworkIdle || false,
browserOptions: {
Expand All @@ -31,6 +30,20 @@ export class PdfService {
return htmlToPdf.convert();
}

async billInvoice(data: any) {
return this.create('bill', data, {
pdf: {
width: '80mm',
margin: {
top: 0.5,
bottom: 0.5,
left: 0.5,
right: 0.5,
},
},
});
}

async tableLabel(data: any) {
return this.create(
'label-table',
Expand Down
18 changes: 18 additions & 0 deletions src/database/entities/core/order.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
import { Exclude } from 'class-transformer';
import { Brackets, Column, Generated, Index, JoinColumn, ManyToOne, OneToMany, SelectQueryBuilder } from 'typeorm';
import { Location } from '../owner/location.entity';
import { Owner } from '../owner/owner.entity';
import { Restaurant } from '../owner/restaurant.entity';
import { Table } from '../owner/table.entity';
import { StaffUser } from '../staff/user.entity';
import { Customer } from './customer.entity';
import { OrderProduct } from './order-product.entity';

Expand Down Expand Up @@ -95,6 +97,22 @@ export class Order extends BaseEntity {
@ManyToOne(() => Location, (loc) => loc.orders, { onDelete: 'CASCADE' })
location: Promise<Location>;

@Exclude()
@ForeignColumn()
staff_id: string;

@JoinColumn()
@ManyToOne(() => StaffUser, (user) => user.orders, { onDelete: 'SET NULL' })
staff: Promise<StaffUser>;

@Exclude()
@ForeignColumn()
owner_id: string;

@JoinColumn()
@ManyToOne(() => Owner, (user) => user.orders, { onDelete: 'SET NULL' })
owner: Promise<Owner>;

@Exclude()
@OneToMany(() => OrderProduct, (op) => op.order)
order_products: Promise<OrderProduct[]>;
Expand Down
7 changes: 6 additions & 1 deletion src/database/entities/owner/owner.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
StatusColumn,
} from '@lib/typeorm/decorators';
import { Exclude } from 'class-transformer';
import { JoinColumn, ManyToOne, OneToOne } from 'typeorm';
import { JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';
import { Order } from '../core/order.entity';
import { Role } from '../core/role.entity';
import { Location } from './location.entity';
import { Restaurant } from './restaurant.entity';
Expand Down Expand Up @@ -85,6 +86,10 @@ export class Owner extends BaseEntity {
@OneToOne(() => Media, (media) => media.owner)
image: Promise<Media>;

@Exclude()
@OneToMany(() => Order, (order) => order.staff)
orders: Promise<Order[]>;

get isVerified() {
return this.verified_at !== null;
}
Expand Down
7 changes: 6 additions & 1 deletion src/database/entities/staff/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
StatusColumn,
} from '@lib/typeorm/decorators';
import { Exclude } from 'class-transformer';
import { JoinColumn, ManyToOne, OneToOne } from 'typeorm';
import { JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';
import { Order } from '../core/order.entity';
import { Restaurant } from '../owner/restaurant.entity';
import { StaffRole } from './role.entity';

Expand Down Expand Up @@ -81,6 +82,10 @@ export class StaffUser extends BaseEntity {
@ManyToOne(() => Restaurant, { onDelete: 'SET NULL' })
restaurant: Promise<Restaurant>;

@Exclude()
@OneToMany(() => Order, (order) => order.staff)
orders: Promise<Order[]>;

get isBlocked() {
return this.status === StaffUserStatus.Blocked;
}
Expand Down
17 changes: 17 additions & 0 deletions src/database/migrations/1726761382132-order-staff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class OrderStaff1726761382132 implements MigrationInterface {
name = 'OrderStaff1726761382132';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`order\` ADD \`staff_id\` varchar(26) NULL`);
await queryRunner.query(
`ALTER TABLE \`order\` ADD CONSTRAINT \`FK_5c126f83b53f2c6c4caafdb914d\` FOREIGN KEY (\`staff_id\`) REFERENCES \`staff_user\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`order\` DROP FOREIGN KEY \`FK_5c126f83b53f2c6c4caafdb914d\``);
await queryRunner.query(`ALTER TABLE \`order\` DROP COLUMN \`staff_id\``);
}
}
17 changes: 17 additions & 0 deletions src/database/migrations/1726762144604-order-owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class OrderOwner1726762144604 implements MigrationInterface {
name = 'OrderOwner1726762144604';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`order\` ADD \`owner_id\` varchar(26) NULL`);
await queryRunner.query(
`ALTER TABLE \`order\` ADD CONSTRAINT \`FK_d9181c2d154dfb71af0e18d9669\` FOREIGN KEY (\`owner_id\`) REFERENCES \`owner\`(\`id\`) ON DELETE SET NULL ON UPDATE NO ACTION`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`order\` DROP FOREIGN KEY \`FK_d9181c2d154dfb71af0e18d9669\``);
await queryRunner.query(`ALTER TABLE \`order\` DROP COLUMN \`owner_id\``);
}
}
Loading

0 comments on commit bc6becb

Please sign in to comment.