Review AI Code. Documented parts that may want to be changed in the future
This commit is contained in:
23
src/index.ts
23
src/index.ts
@@ -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) => {
|
||||||
|
|||||||
@@ -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 [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>());
|
||||||
|
|
||||||
|
|||||||
@@ -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`, {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user