182 lines
5.4 KiB
TypeScript
182 lines
5.4 KiB
TypeScript
import type pg from 'pg';
|
|
import type { OrderRepository } from '../domain/ports.js';
|
|
import type { Order, OrderItem, OrderItemInput, OrderState, OrderView } from '../domain/order.js';
|
|
|
|
interface OrderRow {
|
|
id: string;
|
|
user_id: string;
|
|
idempotency_key: string | null;
|
|
state: OrderState;
|
|
currency: 'EUR';
|
|
subtotal_cents: number;
|
|
discount_cents: number;
|
|
tax_cents: number;
|
|
total_cents: number;
|
|
created_at: Date;
|
|
updated_at: Date;
|
|
}
|
|
interface ItemRow {
|
|
id: string;
|
|
order_id: string;
|
|
product_id: string;
|
|
variant_id: string;
|
|
sku: string;
|
|
ean: string | null;
|
|
name: string;
|
|
unit_price_cents: number;
|
|
discount_cents: number;
|
|
tax_cents: number;
|
|
quantity: number;
|
|
created_at: Date;
|
|
}
|
|
|
|
interface CreateInput {
|
|
userId: string;
|
|
idempotencyKey: string | null;
|
|
items: OrderItemInput[];
|
|
totals: { subtotalCents: number; discountCents: number; taxCents: number; totalCents: number };
|
|
}
|
|
|
|
export class PgOrderRepository implements OrderRepository {
|
|
constructor(private readonly pool: pg.Pool) {}
|
|
|
|
async create(input: CreateInput): Promise<OrderView> {
|
|
const client = await this.pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
const orderResult = await client.query<OrderRow>(
|
|
`INSERT INTO orders_orders (user_id, idempotency_key, subtotal_cents, discount_cents, tax_cents, total_cents)
|
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
|
[
|
|
input.userId,
|
|
input.idempotencyKey,
|
|
input.totals.subtotalCents,
|
|
input.totals.discountCents,
|
|
input.totals.taxCents,
|
|
input.totals.totalCents,
|
|
],
|
|
);
|
|
const order = orderResult.rows[0];
|
|
if (!order) throw new Error('orders_orders INSERT returned no row');
|
|
for (const item of input.items) {
|
|
await client.query<ItemRow>(
|
|
`INSERT INTO orders_items (order_id, product_id, variant_id, sku, ean, name, unit_price_cents, discount_cents, tax_cents, quantity)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`,
|
|
[
|
|
order.id,
|
|
item.productId,
|
|
item.variantId,
|
|
item.sku,
|
|
item.ean,
|
|
item.name,
|
|
item.unitPriceCents,
|
|
item.discountCents,
|
|
item.taxCents,
|
|
item.quantity,
|
|
],
|
|
);
|
|
}
|
|
await client.query('COMMIT');
|
|
const loaded = await this.findById(order.id);
|
|
if (!loaded) throw new Error('orders_orders load after create returned no row');
|
|
return loaded;
|
|
} catch (error) {
|
|
await client.query('ROLLBACK');
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
async findAll(): Promise<OrderView[]> {
|
|
const result = await this.pool.query<OrderRow>(
|
|
'SELECT * FROM orders_orders ORDER BY created_at DESC',
|
|
);
|
|
return Promise.all(
|
|
result.rows.map(async (order) => {
|
|
const items = await this.pool.query<ItemRow>(
|
|
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY id',
|
|
[order.id],
|
|
);
|
|
return { ...toOrder(order), items: items.rows.map(toItem) };
|
|
}),
|
|
);
|
|
}
|
|
|
|
async findById(id: string): Promise<OrderView | undefined> {
|
|
const orderResult = await this.pool.query<OrderRow>(
|
|
'SELECT * FROM orders_orders WHERE id = $1',
|
|
[id],
|
|
);
|
|
const order = orderResult.rows[0];
|
|
if (!order) return undefined;
|
|
const items = await this.pool.query<ItemRow>(
|
|
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
|
[id],
|
|
);
|
|
return { ...toOrder(order), items: items.rows.map(toItem) };
|
|
}
|
|
|
|
async findByIdAndUserId(id: string, userId: string): Promise<OrderView | undefined> {
|
|
const orderResult = await this.pool.query<OrderRow>(
|
|
'SELECT * FROM orders_orders WHERE id = $1 AND user_id = $2',
|
|
[id, userId],
|
|
);
|
|
const order = orderResult.rows[0];
|
|
if (!order) return undefined;
|
|
const items = await this.pool.query<ItemRow>(
|
|
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
|
[id],
|
|
);
|
|
return { ...toOrder(order), items: items.rows.map(toItem) };
|
|
}
|
|
|
|
async updateState(id: string, state: OrderState): Promise<OrderView | undefined> {
|
|
const result = await this.pool.query<OrderRow>(
|
|
`UPDATE orders_orders SET state = $2, updated_at = now()
|
|
WHERE id = $1 RETURNING *`,
|
|
[id, state],
|
|
);
|
|
const order = result.rows[0];
|
|
if (!order) return undefined;
|
|
const items = await this.pool.query<ItemRow>(
|
|
'SELECT * FROM orders_items WHERE order_id = $1 ORDER BY created_at, id',
|
|
[id],
|
|
);
|
|
return { ...toOrder(order), items: items.rows.map(toItem) };
|
|
}
|
|
}
|
|
|
|
function toOrder(row: OrderRow): Order {
|
|
return {
|
|
id: row.id,
|
|
userId: row.user_id,
|
|
idempotencyKey: row.idempotency_key,
|
|
state: row.state,
|
|
currency: row.currency,
|
|
subtotalCents: row.subtotal_cents,
|
|
discountCents: row.discount_cents,
|
|
taxCents: row.tax_cents,
|
|
totalCents: row.total_cents,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at,
|
|
};
|
|
}
|
|
|
|
function toItem(row: ItemRow): OrderItem {
|
|
return {
|
|
id: row.id,
|
|
orderId: row.order_id,
|
|
productId: row.product_id,
|
|
variantId: row.variant_id,
|
|
sku: row.sku,
|
|
ean: row.ean,
|
|
name: row.name,
|
|
unitPriceCents: row.unit_price_cents,
|
|
discountCents: row.discount_cents,
|
|
taxCents: row.tax_cents,
|
|
quantity: row.quantity,
|
|
createdAt: row.created_at,
|
|
};
|
|
}
|