Skip to content

Commit 33e0c19

Browse files
authored
Merge pull request #50 from josecarlospeer-cloud/feat/transaction-lifecycle
2 parents 42d6ae2 + 3b61657 commit 33e0c19

25 files changed

Lines changed: 611 additions & 45 deletions

File tree

src/Apps/HTTP/BillingAPI.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export default class BillingAPI extends HTTPServer {
3131
case "get":
3232
this.http.get(route.path, route.options || {}, route.action);
3333
break;
34+
case "patch":
35+
this.http.patch(route.path, route.options || {}, route.action);
36+
break;
3437
case "post":
3538
this.http.post(route.path, route.options || {}, route.action);
3639
break;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import ConfirmCommand from "@Transaction/Application/Confirm/Command";
2+
import FailCommand from "@Transaction/Application/Fail/Command";
3+
import RefundCommand from "@Transaction/Application/Refund/Command";
4+
import type {FastifyReply, FastifyRequest} from "fastify";
5+
import { Application } from "hollywood-js";
6+
import { IRoute } from "../index";
7+
8+
export function confirm(app: Application.App): IRoute {
9+
return {
10+
action: async (req: FastifyRequest, res: FastifyReply) => {
11+
const { uuid } = req.params as any;
12+
await app.handle(new ConfirmCommand(uuid));
13+
res.status(204).send();
14+
},
15+
method: "patch",
16+
options: {
17+
schema: {
18+
params: {
19+
properties: { uuid: { type: "string" } },
20+
type: "object",
21+
},
22+
response: { 204: {}, 404: {}, 409: {} },
23+
tags: ["Transactions"],
24+
},
25+
},
26+
path: "/transaction/:uuid/confirm",
27+
};
28+
}
29+
30+
export function fail(app: Application.App): IRoute {
31+
return {
32+
action: async (req: FastifyRequest, res: FastifyReply) => {
33+
const { uuid } = req.params as any;
34+
const { reason } = req.body as any;
35+
await app.handle(new FailCommand(uuid, reason ?? ""));
36+
res.status(204).send();
37+
},
38+
method: "patch",
39+
options: {
40+
schema: {
41+
body: {
42+
properties: { reason: { type: "string" } },
43+
type: "object",
44+
},
45+
params: {
46+
properties: { uuid: { type: "string" } },
47+
type: "object",
48+
},
49+
response: { 204: {}, 404: {}, 409: {} },
50+
tags: ["Transactions"],
51+
},
52+
},
53+
path: "/transaction/:uuid/fail",
54+
};
55+
}
56+
57+
export function refund(app: Application.App): IRoute {
58+
return {
59+
action: async (req: FastifyRequest, res: FastifyReply) => {
60+
const { uuid } = req.params as any;
61+
await app.handle(new RefundCommand(uuid));
62+
res.status(204).send();
63+
},
64+
method: "patch",
65+
options: {
66+
schema: {
67+
params: {
68+
properties: { uuid: { type: "string" } },
69+
type: "object",
70+
},
71+
response: { 204: {}, 404: {}, 409: {} },
72+
tags: ["Transactions"],
73+
},
74+
},
75+
path: "/transaction/:uuid/refund",
76+
};
77+
}

src/Apps/HTTP/Routing/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {FastifyReply, FastifyRequest} from "fastify";
22
import type { Application } from "hollywood-js";
33
import health from "./Monitor/Health";
44
import get from "./Transaction/Get";
5+
import { confirm, fail, refund } from "./Transaction/Patch";
56
import create from "./Transaction/Post";
67

78
type Context = (app: Application.App) => IRoute;
@@ -14,7 +15,10 @@ export interface IRoute {
1415
}
1516

1617
export const routes: Context[] = [
17-
get,
18+
confirm,
1819
create,
20+
fail,
21+
get,
1922
health,
23+
refund,
2024
];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Thrown when a domain command is applied to an aggregate in the wrong state.
3+
*
4+
* Source: Vernon (IDDD), ch. 7 — "Domain events model what happened. A full
5+
* lifecycle requires events for each significant state transition, and each
6+
* transition must be guarded by invariants that prevent invalid moves."
7+
*/
8+
export default class InvalidStateException extends Error {
9+
constructor(message: string) {
10+
super(message);
11+
this.name = "InvalidStateException";
12+
}
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ICommand } from "hollywood-js/src/Application";
2+
import TransactionId from "../../Domain/ValueObject/TransactionId";
3+
4+
export default class ConfirmCommand implements ICommand {
5+
public readonly uuid: TransactionId;
6+
7+
constructor(uuid: string) {
8+
this.uuid = new TransactionId(uuid);
9+
}
10+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Application, EventSourcing } from "hollywood-js";
2+
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
3+
import { inject, injectable } from "inversify";
4+
import NotFoundException from "../../../Shared/Domain/Exceptions/NotFoundException";
5+
import Transaction from "../../Domain/Transaction";
6+
import ConfirmCommand from "./Command";
7+
8+
@injectable()
9+
export default class Confirm implements Application.ICommandHandler {
10+
constructor(
11+
@inject("infrastructure.transaction.eventStore")
12+
private readonly writeModel: EventSourcing.EventStore<Transaction>,
13+
) {}
14+
15+
@Application.autowiring
16+
public async handle(command: ConfirmCommand): Promise<void | IAppError> {
17+
try {
18+
const transaction = await this.writeModel.load(command.uuid.toIdentity()) as Transaction;
19+
transaction.confirm();
20+
await this.writeModel.save(transaction);
21+
} catch (err) {
22+
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
23+
throw new NotFoundException("Transaction not found");
24+
}
25+
throw err;
26+
}
27+
}
28+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ICommand } from "hollywood-js/src/Application";
2+
import TransactionId from "../../Domain/ValueObject/TransactionId";
3+
4+
export default class FailCommand implements ICommand {
5+
public readonly uuid: TransactionId;
6+
7+
constructor(
8+
uuid: string,
9+
public readonly reason: string,
10+
) {
11+
this.uuid = new TransactionId(uuid);
12+
}
13+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Application, EventSourcing } from "hollywood-js";
2+
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
3+
import { inject, injectable } from "inversify";
4+
import NotFoundException from "../../../Shared/Domain/Exceptions/NotFoundException";
5+
import Transaction from "../../Domain/Transaction";
6+
import FailCommand from "./Command";
7+
8+
@injectable()
9+
export default class Fail implements Application.ICommandHandler {
10+
constructor(
11+
@inject("infrastructure.transaction.eventStore")
12+
private readonly writeModel: EventSourcing.EventStore<Transaction>,
13+
) {}
14+
15+
@Application.autowiring
16+
public async handle(command: FailCommand): Promise<void | IAppError> {
17+
try {
18+
const transaction = await this.writeModel.load(command.uuid.toIdentity()) as Transaction;
19+
transaction.fail(command.reason);
20+
await this.writeModel.save(transaction);
21+
} catch (err) {
22+
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
23+
throw new NotFoundException("Transaction not found");
24+
}
25+
throw err;
26+
}
27+
}
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ICommand } from "hollywood-js/src/Application";
2+
import TransactionId from "../../Domain/ValueObject/TransactionId";
3+
4+
export default class RefundCommand implements ICommand {
5+
public readonly uuid: TransactionId;
6+
7+
constructor(uuid: string) {
8+
this.uuid = new TransactionId(uuid);
9+
}
10+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Application, EventSourcing } from "hollywood-js";
2+
import type { IAppError } from "hollywood-js/src/Application/Bus/CallbackArg";
3+
import { inject, injectable } from "inversify";
4+
import NotFoundException from "../../../Shared/Domain/Exceptions/NotFoundException";
5+
import Transaction from "../../Domain/Transaction";
6+
import RefundCommand from "./Command";
7+
8+
@injectable()
9+
export default class Refund implements Application.ICommandHandler {
10+
constructor(
11+
@inject("infrastructure.transaction.eventStore")
12+
private readonly writeModel: EventSourcing.EventStore<Transaction>,
13+
) {}
14+
15+
@Application.autowiring
16+
public async handle(command: RefundCommand): Promise<void | IAppError> {
17+
try {
18+
const transaction = await this.writeModel.load(command.uuid.toIdentity()) as Transaction;
19+
transaction.refund();
20+
await this.writeModel.save(transaction);
21+
} catch (err) {
22+
if (err instanceof EventSourcing.AggregateRootNotFoundException) {
23+
throw new NotFoundException("Transaction not found");
24+
}
25+
throw err;
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)