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

View File

@@ -14,6 +14,10 @@ export type ItemsRouteDeps = {
export class ItemsRoute { export class ItemsRoute {
public constructor(private readonly deps: ItemsRouteDeps) {} 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>> { public async getRoutes(): Promise<Array<RouteOptions>> {
return [ return [
{ {

View File

@@ -19,6 +19,10 @@ export type OrdersRouteDeps = {
export class OrdersRoute { export class OrdersRoute {
public constructor(private readonly deps: OrdersRouteDeps) {} 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>> { public async getRoutes(): Promise<Array<RouteOptions>> {
return [ 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) { private async getOrders(_request: FastifyRequest, reply: FastifyReply) {
const orders = await this.deps.database.db const orders = await this.deps.database.db
.selectFrom("orders") .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) { private async getOrder(request: FastifyRequest, reply: FastifyReply) {
const { id } = OrdersRoute.getOrderSchema.parse(request.params); const { id } = OrdersRoute.getOrderSchema.parse(request.params);
// Get the order from the database.
const order = await this.deps.database.db const order = await this.deps.database.db
.selectFrom("orders") .selectFrom("orders")
.selectAll() .selectAll()
.where("id", "=", id) .where("id", "=", id)
.executeTakeFirst(); .executeTakeFirst();
// If the order is not found, return a 404 error.
if (!order) { if (!order) {
return reply.status(404).send({ error: "Order not found" }); return reply.status(404).send({ error: "Order not found" });
} }
// Return the order.
return reply.send({ return reply.send({
...order, ...order,
items: JSON.parse(order.items), 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) { private async createOrder(request: FastifyRequest, reply: FastifyReply) {
const { items: itemsInput } = OrdersRoute.createOrderSchema.parse( const { items: itemsInput } = OrdersRoute.createOrderSchema.parse(
request.body, request.body,
); );
try { try {
// Create the order and payment "channel".
const result = const result =
await this.deps.orderPaymentService.createOrder(itemsInput); await this.deps.orderPaymentService.createOrder(itemsInput);
return reply.status(201).send(result); return reply.status(201).send(result);
} catch (error) { } catch (error) {
// If the error is an order payment error, return a 400 error.
if (error instanceof OrderPaymentError) { if (error instanceof OrderPaymentError) {
return reply.status(error.statusCode).send({ error: error.message }); 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); this.deps.debug("Failed to create order: %o", error);
return reply.status(500).send({ error: "Failed to create order" }); 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({ static getOrderSchema = z.object({
id: z.string(), id: z.string(),
}); });
/**
* Schema for creating an order
* @returns The schema for creating an order
*/
static createOrderSchema = z.object({ static createOrderSchema = z.object({
items: z.array( items: z.array(
z.object({ z.object({

View File

@@ -1,6 +1,7 @@
import "dotenv/config"; import "dotenv/config";
import { z } from "zod"; import { z } from "zod";
// TODO: Decide if zod schema is best. So far, im liking it in the config file.
const configSchema = z.object({ const configSchema = z.object({
engine: z.object({ engine: z.object({
mnemonic: z.string().min(1, "ENGINE_MNEMONIC is required"), 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") .column("invitation_identifier")
.execute(); .execute();
// Hard-Insert items for demo purposes.
const seedItems = [ const seedItems = [
{ {
name: "Cola", name: "Cola",

View File

@@ -11,6 +11,9 @@ export interface APIRoutes {
getRoutes(): Promise<Array<RouteOptions>>; 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. // Zod schema for the API routes.
export const apiRoutesSchema = z.array(z.custom<APIRoutes>()); export const apiRoutesSchema = z.array(z.custom<APIRoutes>());

View File

@@ -66,6 +66,7 @@ export class InvitationSyncClient {
/** /**
* Stateless POST — publish invitation to sync server. * 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> { async publish(invitation: XOInvitation): Promise<XOInvitation> {
const response = await fetch(`${this.normalizedBaseUrl()}/invitations`, { const response = await fetch(`${this.normalizedBaseUrl()}/invitations`, {

View File

@@ -104,6 +104,8 @@ export class OrderInvitationTracker {
return; 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; const hasCustomerActivity = mergedCommits.length > this.initialCommitCount;
if (order.status === "pending" && hasCustomerActivity) { if (order.status === "pending" && hasCustomerActivity) {
@@ -116,7 +118,7 @@ export class OrderInvitationTracker {
this.deps.debug("Order %s marked paid", this.deps.orderId); this.deps.debug("Order %s marked paid", this.deps.orderId);
this.dispenseTimer = setTimeout(() => { this.dispenseTimer = setTimeout(() => {
void this.completeOrder(); this.completeOrder();
}, 1500); }, 1500);
} }
} }

View File

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

View File

@@ -2,6 +2,12 @@ import type { FastifyReply, FastifyRequest } from "fastify";
import debug, { type Debugger } from "debug"; 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. * Represents an event stored in the history buffer.
* Used for replaying missed events to reconnecting clients. * Used for replaying missed events to reconnecting clients.

View File

@@ -12,6 +12,10 @@ export interface Logger {
(...args: unknown[]): void; (...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 { export class Logger {
public readonly namespace!: string; public readonly namespace!: string;
@@ -21,6 +25,7 @@ export class Logger {
namespace: string, namespace: string,
handler: LogHandler = Debug(namespace), handler: LogHandler = Debug(namespace),
) { ) {
// Create a logger function that calls the handler.
const logger = ((...args: Parameters<Debugger>): void => { const logger = ((...args: Parameters<Debugger>): void => {
handler(...args); handler(...args);
}) as Logger; }) as Logger;
@@ -31,6 +36,7 @@ export class Logger {
*/ */
Object.setPrototypeOf(logger, new.target.prototype); Object.setPrototypeOf(logger, new.target.prototype);
// Define the properties for the logger.
Object.defineProperties(logger, { Object.defineProperties(logger, {
namespace: { namespace: {
value: namespace, value: namespace,
@@ -44,6 +50,11 @@ export class Logger {
return 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 { public extend(childNamespace: string): Logger {
return new Logger( return new Logger(
childNamespace, 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 { static isLogger(value: unknown): value is Logger {
return value instanceof Logger; return value instanceof Logger;
} }