Review AI Code. Documented parts that may want to be changed in the future

This commit is contained in:
2026-05-23 18:06:41 +02:00
parent a9f280914d
commit 1a6f6b3bd1
11 changed files with 91 additions and 9 deletions

View File

@@ -21,9 +21,15 @@ type VendingMachineDeps = {
};
export class VendingMachine {
/**
* Create a new VendingMachine instance
* @param config - The configuration for the vending machine
* @returns The VendingMachine instance
*/
static async from(config: Config) {
const debug = new Logger("vending-machine");
// Debug the config.
debug("Config: %O", config);
debug("Creating engine");
@@ -34,13 +40,16 @@ export class VendingMachine {
debug("Creating database");
const database = new Database({ config: config.database, debug });
debug("Creating migration service");
const migrationService = new MigrationService(database);
debug("Migrating database to latest");
await migrationService.migrateToLatest();
// TODO: Do we need to define the trackers outside of the order payment service?
debug("Creating trackers");
const trackers = new Map<string, OrderInvitationTracker>();
debug("Creating order payment service");
const orderPaymentService = await OrderPaymentService.create({
engine,
@@ -81,18 +90,18 @@ export class VendingMachine {
private constructor(private readonly deps: VendingMachineDeps) {}
/**
* Start the vending machine
* @returns A promise that resolves when the vending machine has started
*/
public async start() {
await this.deps.httpService.start();
}
public async stop() {
for (const tracker of this.deps.trackers.values()) {
tracker.stop();
}
await this.deps.engine.stop();
}
}
/**
* Create an instance of the vending machine and start it.
*/
VendingMachine.from(Config.fromEnv())
.then((vendingMachine) => vendingMachine.start())
.catch((error) => {

View File

@@ -14,6 +14,10 @@ export type ItemsRouteDeps = {
export class ItemsRoute {
public constructor(private readonly deps: ItemsRouteDeps) {}
/**
* Get the routes for the items route
* @returns The routes for the items route
*/
public async getRoutes(): Promise<Array<RouteOptions>> {
return [
{

View File

@@ -19,6 +19,10 @@ export type OrdersRouteDeps = {
export class OrdersRoute {
public constructor(private readonly deps: OrdersRouteDeps) {}
/**
* Get the routes for the orders route
* @returns The routes for the orders route
*/
public async getRoutes(): Promise<Array<RouteOptions>> {
return [
{
@@ -39,6 +43,14 @@ export class OrdersRoute {
];
}
/**
* Get all orders from the database
*
* TODO: Do we need this? It should exist on the wallet/state/engine
* @param request
* @param reply
* @returns
*/
private async getOrders(_request: FastifyRequest, reply: FastifyReply) {
const orders = await this.deps.database.db
.selectFrom("orders")
@@ -53,19 +65,28 @@ export class OrdersRoute {
);
}
/**
* Get an order from the database by id
* @param request
* @param reply
* @returns
*/
private async getOrder(request: FastifyRequest, reply: FastifyReply) {
const { id } = OrdersRoute.getOrderSchema.parse(request.params);
// Get the order from the database.
const order = await this.deps.database.db
.selectFrom("orders")
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
// If the order is not found, return a 404 error.
if (!order) {
return reply.status(404).send({ error: "Order not found" });
}
// Return the order.
return reply.send({
...order,
items: JSON.parse(order.items),
@@ -73,29 +94,46 @@ export class OrdersRoute {
});
}
/**
* Create an order and create a payment "channel" for it by creating an invitation and sending it to the sync server.
* @param request
* @param reply
* @returns
*/
private async createOrder(request: FastifyRequest, reply: FastifyReply) {
const { items: itemsInput } = OrdersRoute.createOrderSchema.parse(
request.body,
);
try {
// Create the order and payment "channel".
const result =
await this.deps.orderPaymentService.createOrder(itemsInput);
return reply.status(201).send(result);
} catch (error) {
// If the error is an order payment error, return a 400 error.
if (error instanceof OrderPaymentError) {
return reply.status(error.statusCode).send({ error: error.message });
}
// If the error is not an order payment error, return a 500 error.
this.deps.debug("Failed to create order: %o", error);
return reply.status(500).send({ error: "Failed to create order" });
}
}
/**
* Schema for getting an order by id
* @returns The schema for getting an order by id
*/
static getOrderSchema = z.object({
id: z.string(),
});
/**
* Schema for creating an order
* @returns The schema for creating an order
*/
static createOrderSchema = z.object({
items: z.array(
z.object({

View File

@@ -1,6 +1,7 @@
import "dotenv/config";
import { z } from "zod";
// TODO: Decide if zod schema is best. So far, im liking it in the config file.
const configSchema = z.object({
engine: z.object({
mnemonic: z.string().min(1, "ENGINE_MNEMONIC is required"),

View File

@@ -17,6 +17,7 @@ export async function up(db: Kysely<Database>): Promise<void> {
.column("invitation_identifier")
.execute();
// Hard-Insert items for demo purposes.
const seedItems = [
{
name: "Cola",

View File

@@ -11,6 +11,9 @@ export interface APIRoutes {
getRoutes(): Promise<Array<RouteOptions>>;
}
// TODO: Decide if this complexity in the zod schemas is worth it.
// They are pretty big, and not pretty to look at.
// The value they add is that they allow us to define the defaults in a single place and dont bloat the constructor.
// Zod schema for the API routes.
export const apiRoutesSchema = z.array(z.custom<APIRoutes>());

View File

@@ -66,6 +66,7 @@ export class InvitationSyncClient {
/**
* Stateless POST — publish invitation to sync server.
* TODO: Think about race-conditions here? I think the current sync-server "diffs" them though
*/
async publish(invitation: XOInvitation): Promise<XOInvitation> {
const response = await fetch(`${this.normalizedBaseUrl()}/invitations`, {

View File

@@ -104,6 +104,8 @@ export class OrderInvitationTracker {
return;
}
// TODO: This is the incorrect logic. We probably want to try and either wait for the blockchain or wait for the user to "sign" the invitation
// Basing it off of the commit count is flawed and useless.
const hasCustomerActivity = mergedCommits.length > this.initialCommitCount;
if (order.status === "pending" && hasCustomerActivity) {
@@ -116,7 +118,7 @@ export class OrderInvitationTracker {
this.deps.debug("Order %s marked paid", this.deps.orderId);
this.dispenseTimer = setTimeout(() => {
void this.completeOrder();
this.completeOrder();
}, 1500);
}
}

View File

@@ -172,6 +172,7 @@ export class OrderPaymentService {
roleIdentifier: "merchant",
value: receiptSummary,
},
// TODO: Figure out a better/user-friendly way to display the line items.
{
variableIdentifier: "lineItemsJson",
roleIdentifier: "merchant",

View File

@@ -2,6 +2,12 @@ import type { FastifyReply, FastifyRequest } from "fastify";
import debug, { type Debugger } from "debug";
/**
* TODO: This file was pulled directly from a more advanced project which has multi-user support and replay functionality.
* Its pretty unlikely that you would need replay support for this kind of application because the frontend would likely just grab the data
* and this isn't super real-time. But, I've left the functionality in for now because its better to have the functionality and not need it during development.
*/
/**
* Represents an event stored in the history buffer.
* Used for replaying missed events to reconnecting clients.

View File

@@ -12,6 +12,10 @@ export interface Logger {
(...args: unknown[]): void;
}
/**
* Logger class, similar to 'debug' library but you can call `instanceof Logger` to check if a value is a Logger instance.
* Also, it wont throw if you dont enable debugging but one of the modules want a debug logger. - This feels like a bug in debug though
*/
export class Logger {
public readonly namespace!: string;
@@ -21,6 +25,7 @@ export class Logger {
namespace: string,
handler: LogHandler = Debug(namespace),
) {
// Create a logger function that calls the handler.
const logger = ((...args: Parameters<Debugger>): void => {
handler(...args);
}) as Logger;
@@ -31,6 +36,7 @@ export class Logger {
*/
Object.setPrototypeOf(logger, new.target.prototype);
// Define the properties for the logger.
Object.defineProperties(logger, {
namespace: {
value: namespace,
@@ -44,6 +50,11 @@ export class Logger {
return logger;
}
/**
* Extend the logger with a child namespace
* @param childNamespace - The child namespace to extend the logger with
* @returns The extended logger
*/
public extend(childNamespace: string): Logger {
return new Logger(
childNamespace,
@@ -51,6 +62,11 @@ export class Logger {
);
}
/**
* Check if a value is a Logger instance
* @param value - The value to check
* @returns True if the value is a Logger instance, false otherwise
*/
static isLogger(value: unknown): value is Logger {
return value instanceof Logger;
}