Skip to content

Commit

Permalink
improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
marcus-sa committed Aug 24, 2024
1 parent c1d52d1 commit 66604ff
Show file tree
Hide file tree
Showing 26 changed files with 345 additions and 69 deletions.
Binary file modified bun.lockb
Binary file not shown.
4 changes: 4 additions & 0 deletions common/src/lib/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ export class Money {
static ZERO = new Money(0);
static MAX = new Money(Number.MAX_SAFE_INTEGER);

get cents(): number {
return this.amount * 100;
}

constructor(readonly amount: float) {}

add(delta: Money): Money {
Expand Down
3 changes: 3 additions & 0 deletions common/src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
import { entity } from '@deepkit/type';

@entity.name('@error/unsupported-state-transition-exception')
export class UnsupportedStateTransitionException extends Error {}
2 changes: 1 addition & 1 deletion common/src/lib/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class RestateRepository<E extends OrmEntity> {
.findOneOrUndefined();
}

async persist(entity: E): Promise<void> {
async save(entity: E): Promise<void> {
await this.#ctx.run(() => this.database.persist(entity));
}

Expand Down
2 changes: 2 additions & 0 deletions common/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Unique } from '@deepkit/type';

export interface PersonName {
readonly firstName: string;
readonly lastName: string;
Expand Down
18 changes: 16 additions & 2 deletions customer-service-api/src/lib/entities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { entity, PrimaryKey, uuid, UUID } from '@deepkit/type';
import {
Email,
entity,
JSONPartial,
PrimaryKey,
Unique,
uuid,
UUID,
} from '@deepkit/type';
import { Writable } from 'type-fest';

import { PersonName } from '@ftgo/common';
Expand All @@ -10,7 +18,13 @@ export class Customer {
readonly id: UUID & PrimaryKey = uuid();
readonly disabled: boolean = false;

constructor(readonly name: PersonName) {}
readonly name: PersonName;
readonly email: Email & Unique;
readonly phoneNumber?: string & Unique;

static create(data: JSONPartial<Customer>) {
return Object.assign(new Customer(), data);
}

assertEnabled(): void {
if (!this.disabled) {
Expand Down
11 changes: 9 additions & 2 deletions customer-service-api/src/lib/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Customer } from './entities';
import { Email, UUID } from '@deepkit/type';

import { PersonName } from '@ftgo/common';

export class CustomerCreatedEvent {
constructor(readonly customer: Customer) {}
constructor(
readonly id: UUID,
readonly name: PersonName,
readonly email: Email,
readonly phoneNumber?: string,
) {}
}
5 changes: 1 addition & 4 deletions delivery-service-api/src/lib/services.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { RestateClient, RestateService } from 'deepkit-restate';
import { typeOf, UUID } from '@deepkit/type';

import { FactoryProvider } from '@deepkit/injector';
import { RestateService } from 'deepkit-restate';

export interface DeliveryServiceHandlers {}

Expand Down
11 changes: 11 additions & 0 deletions kitchen-service-api/src/lib/entities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { entity, integer, JSONEntity, Positive, UUID } from '@deepkit/type';
import { Writable } from 'type-fest';

import { UnsupportedStateTransitionException } from '@ftgo/common';

@entity.name('kitchen')
export class Kitchen {
static create(data: JSONEntity<Kitchen>): Kitchen {
Expand Down Expand Up @@ -35,11 +37,20 @@ export class Ticket {
}

cancel(this: Writable<this>) {
if (
this.state !== TicketState.CREATED &&
this.state !== TicketState.CONFIRMED
) {
throw new UnsupportedStateTransitionException(this.state);
}
this.state = TicketState.CANCELLED;
// delete this.confirmCancelAwakeableId;
}

confirm(this: Writable<this>): void {
if (this.state !== TicketState.CREATED) {
throw new UnsupportedStateTransitionException(this.state);
}
this.state = TicketState.CONFIRMED;
// delete this.confirmCreateAwakeableId;
}
Expand Down
4 changes: 2 additions & 2 deletions kitchen-service/src/kitchen.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class KitchenService implements KitchenServiceHandlers {
throw new TicketNotFound(id);
}
ticket.cancel();
await this.ticket.persist(ticket);
await this.ticket.save(ticket);
await this.ctx.rejectAwakeable(
ticket.confirmCreateAwakeableId!,
CreateOrderSagaState.CANCELLED,
Expand All @@ -59,7 +59,7 @@ export class KitchenService implements KitchenServiceHandlers {
throw new TicketNotFound(id);
}
ticket.confirm();
await this.ticket.persist(ticket);
await this.ticket.save(ticket);
await this.ctx.resolveAwakeable<TicketConfirmed>(
ticket.confirmCreateAwakeableId!,
new TicketConfirmed(readyAt),
Expand Down
8 changes: 8 additions & 0 deletions order-service-api/src/lib/replies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export class OrderNotFound extends Error {
}
}

export class OrderApproved {
constructor() {}
}

export class OrderRejected {
constructor() {}
}

export class OrderRevisionProposed {
constructor(
readonly revision: OrderRevision,
Expand Down
7 changes: 5 additions & 2 deletions order-service-api/src/lib/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import { OrderDetails, OrderRevision } from './entities';
export enum CreateOrderSagaState {
STARTED = 'STARTED',
CUSTOMER_VALIDATED = 'CUSTOMER_VALIDATED',
PAYMENT_RESERVED = 'PAYMENT_RESERVED',
CANCELLED = 'CANCELLED',
PAYMENT_AUTHORIZED = 'PAYMENT_AUTHORIZED',
WAITING_FOR_CONFIRMATION = 'WAITING_FOR_CONFIRMATION',
REJECTED = 'REJECTED',
CONFIRMED = 'CONFIRMED',
APPROVED = 'APPROVED',
}

export class CreateOrderSagaData {
Expand Down
11 changes: 7 additions & 4 deletions order-service-api/src/lib/services.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { restate, RestateService } from 'deepkit-restate';
import { UUID } from '@deepkit/type';
import { ItemNotFound } from '@deepkit/orm';

import {
Restaurant,
Expand All @@ -10,7 +9,11 @@ import {

import { Order } from './entities';
import { CreateOrderRequest } from './dtos';
import { OrderMinimumNotMetException } from './replies';
import {
OrderApproved,
OrderMinimumNotMetException,
OrderNotFound,
} from './replies';

export interface OrderServiceHandlers {
create(request: CreateOrderRequest): Promise<Order>;
Expand All @@ -21,13 +24,13 @@ export interface OrderServiceHandlers {
cancel(id: UUID): Promise<Order>;
confirmCancel(id: UUID): Promise<Order>;
reject(id: UUID): Promise<Order>;
approve(id: UUID): Promise<Order>;
approve(id: UUID): Promise<OrderApproved>;
createMenu(event: RestaurantCreatedEvent): Promise<void>;
reviseMenu(event: RestaurantCreatedEvent): Promise<void>;
}

export type OrderServiceApi = RestateService<
'Order',
OrderServiceHandlers,
[ItemNotFound, OrderMinimumNotMetException]
[OrderNotFound, OrderMinimumNotMetException]
>;
2 changes: 1 addition & 1 deletion order-service/src/order.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class OrderService implements OrderServiceHandlers {
throw new OrderNotFound(id);
}
order.cancel();
await this.order.persist(order);
await this.order.save(order);
return order;
}

Expand Down
54 changes: 30 additions & 24 deletions order-service/src/sagas/create-order.saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { cast } from '@deepkit/type';
import { Writable } from 'type-fest';

import { CustomerServiceApi } from '@ftgo/customer-service-api';
import { PaymentReserved, PaymentServiceApi } from '@ftgo/payment-service-api';
import {
PaymentAuthorized,
PaymentServiceApi,
} from '@ftgo/payment-service-api';
import {
KitchenServiceApi,
Ticket,
TicketConfirmed,
TicketCreated,
TicketDetails,
Expand All @@ -20,6 +22,8 @@ import {
CreateOrderSagaApi,
CreateOrderSagaData,
CreateOrderSagaState,
OrderApproved,
OrderRejected,
OrderServiceApi,
} from '@ftgo/order-service-api';

Expand All @@ -29,22 +33,22 @@ export class CreateOrderSaga extends Saga<CreateOrderSagaData> {

readonly definition = this.step()
.compensate(this.reject)
.onReply<OrderRejected>(this.handleRejected)
.step()
.invoke(this.validate)
.step()
.invoke(this.reservePayment)
.onReply<PaymentReserved>(this.handlePaymentReserved)
.compensate(this.reversePayment)
.invoke(this.authorizePayment)
.onReply<PaymentAuthorized>(this.handlePaymentAuthorized)
.compensate(this.reversePaymentAuthorization)
.step()
.invoke(this.createTicket)
.onReply<TicketCreated>(this.handleTicketCreated)
.compensate(this.cancelTicket)
.step()
.invoke(this.authorize)
.step()
.invoke(this.waitForTicketConfirmation)
.step()
.invoke(this.approve)
.onReply<OrderApproved>(this.handleApproved)
.build();

constructor(
Expand All @@ -61,34 +65,38 @@ export class CreateOrderSaga extends Saga<CreateOrderSagaData> {
return this.order.reject(orderId);
}

handleRejected(data: Writable<CreateOrderSagaData>) {
data.state = CreateOrderSagaState.REJECTED;
}

validate({
orderDetails: { customerId, orderTotal },
orderId,
}: CreateOrderSagaData) {
// validate that customer can reserve the money
// validate that customer can authorize the money
return this.customer.validateOrder(customerId, orderId, orderTotal);
}

reservePayment({
authorizePayment({
orderDetails: { customerId, orderTotal },
orderId,
}: CreateOrderSagaData) {
return this.payment.reserve(customerId, orderId, orderTotal);
return this.payment.authorize(customerId, orderId, orderTotal);
}

handlePaymentReserved(
handlePaymentAuthorized(
data: Writable<CreateOrderSagaData>,
{ paymentId }: PaymentReserved,
{ paymentId }: PaymentAuthorized,
) {
data.paymentId = paymentId;
data.state = CreateOrderSagaState.PAYMENT_RESERVED;
data.state = CreateOrderSagaState.PAYMENT_AUTHORIZED;
}

reversePayment({ paymentId }: CreateOrderSagaData) {
reversePaymentAuthorization({ paymentId }: CreateOrderSagaData) {
if (!paymentId) {
throw new Error('Missing payment id');
}
return this.payment.reverse(paymentId);
return this.payment.reverseAuthorization(paymentId);
}

async createTicket({
Expand All @@ -109,6 +117,7 @@ export class CreateOrderSaga extends Saga<CreateOrderSagaData> {
data: Writable<CreateOrderSagaData>,
{ ticketId }: TicketCreated,
) {
data.state = CreateOrderSagaState.WAITING_FOR_CONFIRMATION;
data.ticketId = ticketId;
}

Expand All @@ -119,19 +128,16 @@ export class CreateOrderSaga extends Saga<CreateOrderSagaData> {
return this.kitchen.cancelTicket(ticketId);
}

// TODO: should payment be processed upon ticket confirmation or after delivery?
authorize({
orderDetails: { customerId, orderTotal },
orderId,
}: CreateOrderSagaData) {
return this.payment.authorize(customerId, orderId, orderTotal);
}

async waitForTicketConfirmation() {
async waitForTicketConfirmation(data: Writable<CreateOrderSagaData>) {
await this.#confirmTicketAwakeable!.promise;
data.state = CreateOrderSagaState.CONFIRMED;
}

approve({ orderId }: CreateOrderSagaData) {
return this.order.approve(orderId);
}

handleApproved(data: Writable<CreateOrderSagaData>) {
data.state = CreateOrderSagaState.APPROVED;
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@restatedev/restate-sdk": "1.2.0",
"deepkit-restate": "0.0.47",
"kafkajs": "2.2.4",
"stripe": "^16.8.0",
"tslib": "2.6.3"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit 66604ff

Please sign in to comment.