last test fixes for upcoming V0.1 + pretty
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m40s

This commit is contained in:
2025-12-23 17:20:51 -08:00
parent 3669958e9d
commit 186ed484b7
26 changed files with 149 additions and 113 deletions

View File

@@ -25,7 +25,7 @@ jobs:
DB_USER: ${{ secrets.DB_USER }} DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_DATABASE_PROD }} DB_NAME: ${{ secrets.DB_DATABASE_PROD }}
BACKUP_DIR: "/var/www/backups" # Define a dedicated directory for backups BACKUP_DIR: '/var/www/backups' # Define a dedicated directory for backups
steps: steps:
- name: Validate Secrets and Inputs - name: Validate Secrets and Inputs

View File

@@ -34,7 +34,7 @@ We will adopt a strict, consistent error-handling contract for the service and r
**Robustness**: Eliminates an entire class of bugs where `undefined` is passed to `res.json()`, preventing incorrect `500` errors. **Robustness**: Eliminates an entire class of bugs where `undefined` is passed to `res.json()`, preventing incorrect `500` errors.
**Consistency & Predictability**: All data-fetching methods now have a predictable contract. They either return the expected data or throw a specific, typed error. **Consistency & Predictability**: All data-fetching methods now have a predictable contract. They either return the expected data or throw a specific, typed error.
**Developer Experience**: Route handlers become simpler, cleaner, and easier to write correctly. The cognitive load on developers is reduced as they no longer need to remember to check for `undefined`. **Developer Experience**: Route handlers become simpler, cleaner, and easier to write correctly. The cognitive load on developers is reduced as they no longer need to remember to check for `undefined`.
**Improved Testability**: Tests become more reliable and realistic. Mocks can now throw the *exact* error type (`new NotFoundError()`) that the real implementation would, ensuring tests accurately reflect the application's behavior. **Improved Testability**: Tests become more reliable and realistic. Mocks can now throw the _exact_ error type (`new NotFoundError()`) that the real implementation would, ensuring tests accurately reflect the application's behavior.
**Centralized Control**: Error-to-HTTP-status logic is centralized in the `errorHandler` middleware, making it easy to manage and modify error responses globally. **Centralized Control**: Error-to-HTTP-status logic is centralized in the `errorHandler` middleware, making it easy to manage and modify error responses globally.
### Negative ### Negative

View File

@@ -10,21 +10,19 @@ Following the standardization of error handling in ADR-001, the next most common
This manual approach has several drawbacks: This manual approach has several drawbacks:
**Repetitive Boilerplate**: The `try/catch/finally` block for transaction management is duplicated across multiple files. **Repetitive Boilerplate**: The `try/catch/finally` block for transaction management is duplicated across multiple files.
**Error-Prone**: It is easy to forget to `client.release()` in all code paths, which can lead to connection pool exhaustion and bring down the application. **Error-Prone**: It is easy to forget to `client.release()` in all code paths, which can lead to connection pool exhaustion and bring down the application. 3. **Poor Composability**: It is difficult to compose multiple repository methods into a single, atomic "Unit of Work". For example, a service function that needs to update a user's points and create a budget in a single transaction cannot easily do so if both underlying repository methods create their own transactions.
3. **Poor Composability**: It is difficult to compose multiple repository methods into a single, atomic "Unit of Work". For example, a service function that needs to update a user's points and create a budget in a single transaction cannot easily do so if both underlying repository methods create their own transactions.
## Decision ## Decision
We will implement a standardized "Unit of Work" pattern through a high-level `withTransaction` helper function. This function will abstract away the complexity of transaction management. We will implement a standardized "Unit of Work" pattern through a high-level `withTransaction` helper function. This function will abstract away the complexity of transaction management.
1. **`withTransaction` Helper**: A new helper function, `withTransaction<T>(callback: (client: PoolClient) => Promise<T>): Promise<T>`, will be created. This function will be responsible for: 1. **`withTransaction` Helper**: A new helper function, `withTransaction<T>(callback: (client: PoolClient) => Promise<T>): Promise<T>`, will be created. This function will be responsible for:
- Acquiring a client from the database pool.
* Acquiring a client from the database pool. - Starting a transaction (`BEGIN`).
* Starting a transaction (`BEGIN`). - Executing the `callback` function, passing the transactional client to it.
* Executing the `callback` function, passing the transactional client to it. - If the callback succeeds, it will `COMMIT` the transaction.
* If the callback succeeds, it will `COMMIT` the transaction. - If the callback throws an error, it will `ROLLBACK` the transaction and re-throw the error.
* If the callback throws an error, it will `ROLLBACK` the transaction and re-throw the error. - In all cases, it will `RELEASE` the client back to the pool.
* In all cases, it will `RELEASE` the client back to the pool.
2. **Repository Method Signature**: Repository methods that need to be part of a transaction will be updated to optionally accept a `PoolClient` in their constructor or as a method parameter. By default, they will use the global pool. When called from within a `withTransaction` block, they will be passed the transactional client. 2. **Repository Method Signature**: Repository methods that need to be part of a transaction will be updated to optionally accept a `PoolClient` in their constructor or as a method parameter. By default, they will use the global pool. When called from within a `withTransaction` block, they will be passed the transactional client.
3. **Service Layer Orchestration**: Service-layer functions that orchestrate multi-step operations will use `withTransaction` to ensure atomicity. They will instantiate or call repository methods, providing them with the transactional client from the callback. 3. **Service Layer Orchestration**: Service-layer functions that orchestrate multi-step operations will use `withTransaction` to ensure atomicity. They will instantiate or call repository methods, providing them with the transactional client from the callback.
@@ -40,7 +38,7 @@ async function registerUserAndCreateDefaultList(userData) {
const shoppingRepo = new ShoppingRepository(client); const shoppingRepo = new ShoppingRepository(client);
const newUser = await userRepo.createUser(userData); const newUser = await userRepo.createUser(userData);
await shoppingRepo.createShoppingList(newUser.user_id, "My First List"); await shoppingRepo.createShoppingList(newUser.user_id, 'My First List');
return newUser; return newUser;
}); });

View File

@@ -20,8 +20,8 @@ We will adopt a schema-based approach for input validation using the `zod` libra
1. **Adopt `zod` for Schema Definition**: We will use `zod` to define clear, type-safe schemas for the `params`, `query`, and `body` of each API request. `zod` provides powerful and declarative validation rules and automatically infers TypeScript types. 1. **Adopt `zod` for Schema Definition**: We will use `zod` to define clear, type-safe schemas for the `params`, `query`, and `body` of each API request. `zod` provides powerful and declarative validation rules and automatically infers TypeScript types.
2. **Create a Reusable Validation Middleware**: A generic `validateRequest(schema)` middleware will be created. This middleware will take a `zod` schema, parse the incoming request against it, and handle success and error cases. 2. **Create a Reusable Validation Middleware**: A generic `validateRequest(schema)` middleware will be created. This middleware will take a `zod` schema, parse the incoming request against it, and handle success and error cases.
* On successful validation, the parsed and typed data will be attached to the `req` object (e.g., `req.body` will be replaced with the parsed body), and `next()` will be called. - On successful validation, the parsed and typed data will be attached to the `req` object (e.g., `req.body` will be replaced with the parsed body), and `next()` will be called.
* On validation failure, the middleware will call `next()` with a custom `ValidationError` containing a structured list of issues, which `ADR-001`'s `errorHandler` can then format into a user-friendly `400 Bad Request` response. - On validation failure, the middleware will call `next()` with a custom `ValidationError` containing a structured list of issues, which `ADR-001`'s `errorHandler` can then format into a user-friendly `400 Bad Request` response.
3. **Refactor Routes**: All route handlers will be refactored to use this new middleware, removing all manual validation logic. 3. **Refactor Routes**: All route handlers will be refactored to use this new middleware, removing all manual validation logic.
@@ -46,18 +46,18 @@ const getFlyerSchema = z.object({
type GetFlyerRequest = z.infer<typeof getFlyerSchema>; type GetFlyerRequest = z.infer<typeof getFlyerSchema>;
// 3. Apply the middleware and use an inline cast for the request // 3. Apply the middleware and use an inline cast for the request
router.get('/:id', validateRequest(getFlyerSchema), (async (req, res, next) => { router.get('/:id', validateRequest(getFlyerSchema), async (req, res, next) => {
// Cast 'req' to the inferred type. // Cast 'req' to the inferred type.
// This provides full type safety for params, query, and body. // This provides full type safety for params, query, and body.
const { params } = req as unknown as GetFlyerRequest; const { params } = req as unknown as GetFlyerRequest;
try { try {
const flyer = await db.flyerRepo.getFlyerById(params.id); // params.id is 'number' const flyer = await db.flyerRepo.getFlyerById(params.id); // params.id is 'number'
res.json(flyer); res.json(flyer);
} catch (error) { } catch (error) {
next(error); next(error);
} }
})); });
``` ```
## Consequences ## Consequences

View File

@@ -20,9 +20,9 @@ We will adopt a standardized, application-wide structured logging policy. All lo
**Request-Scoped Logger with Context**: We will create a middleware that runs at the beginning of the request lifecycle. This middleware will: **Request-Scoped Logger with Context**: We will create a middleware that runs at the beginning of the request lifecycle. This middleware will:
* Generate a unique `request_id` for each incoming request. - Generate a unique `request_id` for each incoming request.
* Create a request-scoped logger instance (a "child logger") that automatically includes the `request_id`, `user_id` (if authenticated), and `ip_address` in every log message it generates. - Create a request-scoped logger instance (a "child logger") that automatically includes the `request_id`, `user_id` (if authenticated), and `ip_address` in every log message it generates.
* Attach this child logger to the `req` object (e.g., `req.log`). - Attach this child logger to the `req` object (e.g., `req.log`).
**Mandatory Use of Request-Scoped Logger**: All route handlers and any service functions called by them **MUST** use the request-scoped logger (`req.log`) instead of the global logger instance. This ensures all logs for a given request are automatically correlated. **Mandatory Use of Request-Scoped Logger**: All route handlers and any service functions called by them **MUST** use the request-scoped logger (`req.log`) instead of the global logger instance. This ensures all logs for a given request are automatically correlated.
@@ -32,9 +32,9 @@ We will adopt a standardized, application-wide structured logging policy. All lo
**Standardized Logging Practices**: **Standardized Logging Practices**:
**INFO**: Log key business events, such as `User logged in` or `Flyer processed`. **INFO**: Log key business events, such as `User logged in` or `Flyer processed`.
**WARN**: Log recoverable errors or unusual situations that do not break the request, such as `Client Error: 404 on GET /api/non-existent-route` or `Retrying failed database connection`. **WARN**: Log recoverable errors or unusual situations that do not break the request, such as `Client Error: 404 on GET /api/non-existent-route` or `Retrying failed database connection`.
**ERROR**: Log only unhandled or server-side errors that cause a request to fail (typically handled by the `errorHandler`). Avoid logging expected client errors (like 4xx) at this level. **ERROR**: Log only unhandled or server-side errors that cause a request to fail (typically handled by the `errorHandler`). Avoid logging expected client errors (like 4xx) at this level.
**DEBUG**: Log detailed diagnostic information useful during development, such as function entry/exit points or variable states. **DEBUG**: Log detailed diagnostic information useful during development, such as function entry/exit points or variable states.
### Example Usage ### Example Usage
@@ -59,15 +59,15 @@ export const requestLogger = (req, res, next) => {
// In a route handler: // In a route handler:
router.get('/:id', async (req, res, next) => { router.get('/:id', async (req, res, next) => {
// Use the request-scoped logger // Use the request-scoped logger
req.log.info({ flyerId: req.params.id }, 'Fetching flyer by ID'); req.log.info({ flyerId: req.params.id }, 'Fetching flyer by ID');
try { try {
// ... business logic ... // ... business logic ...
res.json(flyer); res.json(flyer);
} catch (error) { } catch (error) {
// The error itself will be logged with full context by the errorHandler // The error itself will be logged with full context by the errorHandler
next(error); next(error);
} }
}); });
``` ```

View File

@@ -14,5 +14,5 @@ We will formalize a centralized Role-Based Access Control (RBAC) or Attribute-Ba
## Consequences ## Consequences
* **Positive**: Ensures authorization logic is consistent, easy to audit, and decoupled from business logic. Improves security by centralizing access control. - **Positive**: Ensures authorization logic is consistent, easy to audit, and decoupled from business logic. Improves security by centralizing access control.
* **Negative**: Requires a significant refactoring effort to integrate the new authorization system across all protected routes and features. Introduces a new dependency if an external library is chosen. - **Negative**: Requires a significant refactoring effort to integrate the new authorization system across all protected routes and features. Introduces a new dependency if an external library is chosen.

View File

@@ -14,5 +14,5 @@ We will establish a formal Design System and Component Library. This will involv
## Consequences ## Consequences
* **Positive**: Ensures a consistent and high-quality user interface. Accelerates frontend development by providing reusable, well-documented components. Improves maintainability and reduces technical debt. - **Positive**: Ensures a consistent and high-quality user interface. Accelerates frontend development by providing reusable, well-documented components. Improves maintainability and reduces technical debt.
* **Negative**: Requires an initial investment in setting up Storybook and migrating existing components. Adds a new dependency and a new workflow for frontend development. - **Negative**: Requires an initial investment in setting up Storybook and migrating existing components. Adds a new dependency and a new workflow for frontend development.

View File

@@ -14,5 +14,5 @@ We will adopt a dedicated database migration tool, such as **`node-pg-migrate`**
## Consequences ## Consequences
* **Positive**: Provides a safe, repeatable, and reversible way to evolve the database schema. Improves team collaboration on database changes. Reduces the risk of data loss or downtime during deployments. - **Positive**: Provides a safe, repeatable, and reversible way to evolve the database schema. Improves team collaboration on database changes. Reduces the risk of data loss or downtime during deployments.
* **Negative**: Requires an initial setup and learning curve for the chosen migration tool. All future schema changes must adhere to the migration workflow. - **Negative**: Requires an initial setup and learning curve for the chosen migration tool. All future schema changes must adhere to the migration workflow.

View File

@@ -14,5 +14,5 @@ We will standardize the deployment process by containerizing the application usi
## Consequences ## Consequences
* **Positive**: Ensures consistency between development and production environments. Simplifies the setup for new developers. Improves portability and scalability of the application. - **Positive**: Ensures consistency between development and production environments. Simplifies the setup for new developers. Improves portability and scalability of the application.
* **Negative**: Requires learning Docker and containerization concepts. Adds `Dockerfile` and `docker-compose.yml` to the project's configuration. - **Negative**: Requires learning Docker and containerization concepts. Adds `Dockerfile` and `docker-compose.yml` to the project's configuration.

View File

@@ -18,5 +18,5 @@ We will implement a multi-layered security approach for the API:
## Consequences ## Consequences
* **Positive**: Significantly improves the application's security posture against common web vulnerabilities like XSS, clickjacking, and brute-force attacks. - **Positive**: Significantly improves the application's security posture against common web vulnerabilities like XSS, clickjacking, and brute-force attacks.
* **Negative**: Requires careful configuration of CORS and rate limits to avoid blocking legitimate traffic. Content-Security-Policy can be complex to configure correctly. - **Negative**: Requires careful configuration of CORS and rate limits to avoid blocking legitimate traffic. Content-Security-Policy can be complex to configure correctly.

View File

@@ -14,5 +14,5 @@ We will formalize the end-to-end CI/CD process. This ADR will define the project
## Consequences ## Consequences
* **Positive**: Automates quality control and creates a safe, repeatable path to production. Increases development velocity and reduces deployment-related errors. - **Positive**: Automates quality control and creates a safe, repeatable path to production. Increases development velocity and reduces deployment-related errors.
* **Negative**: Initial setup effort for the CI/CD pipeline. May slightly increase the time to merge code due to mandatory checks. - **Negative**: Initial setup effort for the CI/CD pipeline. May slightly increase the time to merge code due to mandatory checks.

View File

@@ -14,5 +14,5 @@ We will adopt **OpenAPI (Swagger)** for API documentation. We will use tools (e.
## Consequences ## Consequences
* **Positive**: Creates a single source of truth for API documentation that stays in sync with the code. Enables auto-generation of client SDKs and simplifies testing. - **Positive**: Creates a single source of truth for API documentation that stays in sync with the code. Enables auto-generation of client SDKs and simplifies testing.
* **Negative**: Requires developers to maintain JSDoc annotations on all routes. Adds a build step and new dependencies to the project. - **Negative**: Requires developers to maintain JSDoc annotations on all routes. Adds a build step and new dependencies to the project.

View File

@@ -14,5 +14,5 @@ We will implement a formal data backup and recovery strategy. This will involve
## Consequences ## Consequences
* **Positive**: Protects against catastrophic data loss, ensuring business continuity. Provides a clear, tested plan for disaster recovery. - **Positive**: Protects against catastrophic data loss, ensuring business continuity. Provides a clear, tested plan for disaster recovery.
* **Negative**: Requires setup and maintenance of backup scripts and secure storage. Incurs storage costs for backup files. - **Negative**: Requires setup and maintenance of backup scripts and secure storage. Incurs storage costs for backup files.

View File

@@ -12,11 +12,11 @@ When the application is containerized (`ADR-014`), the container orchestrator (e
We will implement dedicated health check endpoints in the Express application. We will implement dedicated health check endpoints in the Express application.
* A **Liveness Probe** (`/api/health/live`) will return a `200 OK` to indicate the server is running. If it fails, the orchestrator should restart the container. - A **Liveness Probe** (`/api/health/live`) will return a `200 OK` to indicate the server is running. If it fails, the orchestrator should restart the container.
* A **Readiness Probe** (`/api/health/ready`) will return a `200 OK` only if the application is ready to accept traffic (e.g., database connection is established). If it fails, the orchestrator will temporarily remove the container from the load balancer. - A **Readiness Probe** (`/api/health/ready`) will return a `200 OK` only if the application is ready to accept traffic (e.g., database connection is established). If it fails, the orchestrator will temporarily remove the container from the load balancer.
## Consequences ## Consequences
* **Positive**: Enables robust, automated application lifecycle management in a containerized environment. Prevents traffic from being sent to unhealthy or uninitialized application instances. - **Positive**: Enables robust, automated application lifecycle management in a containerized environment. Prevents traffic from being sent to unhealthy or uninitialized application instances.
* **Negative**: Adds a small amount of code for the health check endpoints. Requires configuration in the container orchestration layer. - **Negative**: Adds a small amount of code for the health check endpoints. Requires configuration in the container orchestration layer.

View File

@@ -24,8 +24,8 @@ We will adopt a standardized, application-wide structured logging policy for all
**2. Pino-like API for Structured Logging**: The client logger mimics the `pino` API, which is the standard on the backend. It supports two primary call signatures: **2. Pino-like API for Structured Logging**: The client logger mimics the `pino` API, which is the standard on the backend. It supports two primary call signatures:
* `logger.info('A simple message');` - `logger.info('A simple message');`
* `logger.info({ key: 'value' }, 'A message with a structured data payload');` - `logger.info({ key: 'value' }, 'A message with a structured data payload');`
The second signature, which includes a data object as the first argument, is **strongly preferred**, especially for logging errors or complex state. The second signature, which includes a data object as the first argument, is **strongly preferred**, especially for logging errors or complex state.
@@ -79,7 +79,7 @@ describe('MyComponent', () => {
// Assert that the logger was called with the expected structure // Assert that the logger was called with the expected structure
expect(logger.error).toHaveBeenCalledWith( expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({ err: expect.any(Error) }), // Check for the error object expect.objectContaining({ err: expect.any(Error) }), // Check for the error object
'Failed to fetch component data' // Check for the message 'Failed to fetch component data', // Check for the message
); );
}); });
}); });

View File

@@ -111,7 +111,7 @@ CREATE TABLE IF NOT EXISTS public.flyers (
image_url TEXT NOT NULL, image_url TEXT NOT NULL,
icon_url TEXT, icon_url TEXT,
checksum TEXT UNIQUE, checksum TEXT UNIQUE,
store_id BIGINT REFERENCES public.stores(store_id), store_id BIGINT REFERENCES public.stores(store_id) ON DELETE CASCADE,
valid_from DATE, valid_from DATE,
valid_to DATE, valid_to DATE,
store_address TEXT, store_address TEXT,
@@ -139,7 +139,7 @@ CREATE INDEX IF NOT EXISTS idx_flyers_valid_to_file_name ON public.flyers (valid
CREATE TABLE IF NOT EXISTS public.master_grocery_items ( CREATE TABLE IF NOT EXISTS public.master_grocery_items (
master_grocery_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_grocery_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
category_id BIGINT REFERENCES public.categories(category_id), category_id BIGINT REFERENCES public.categories(category_id) ON DELETE SET NULL,
is_allergen BOOLEAN DEFAULT false, is_allergen BOOLEAN DEFAULT false,
allergy_info JSONB, allergy_info JSONB,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
@@ -171,13 +171,13 @@ CREATE TABLE IF NOT EXISTS public.flyer_items (
price_in_cents INTEGER, price_in_cents INTEGER,
quantity_num NUMERIC, quantity_num NUMERIC,
quantity TEXT NOT NULL, quantity TEXT NOT NULL,
category_id BIGINT REFERENCES public.categories(category_id), category_id BIGINT REFERENCES public.categories(category_id) ON DELETE SET NULL,
category_name TEXT, category_name TEXT,
unit_price JSONB, unit_price JSONB,
view_count INTEGER DEFAULT 0 NOT NULL, view_count INTEGER DEFAULT 0 NOT NULL,
click_count INTEGER DEFAULT 0 NOT NULL, click_count INTEGER DEFAULT 0 NOT NULL,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE SET NULL,
product_id BIGINT, product_id BIGINT REFERENCES public.products(product_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
); );
@@ -294,7 +294,7 @@ CREATE INDEX IF NOT EXISTS idx_shopping_lists_user_id ON public.shopping_lists(u
CREATE TABLE IF NOT EXISTS public.shopping_list_items ( CREATE TABLE IF NOT EXISTS public.shopping_list_items (
shopping_list_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_list_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(shopping_list_id) ON DELETE CASCADE, shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(shopping_list_id) ON DELETE CASCADE,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
custom_item_name TEXT, custom_item_name TEXT,
quantity NUMERIC DEFAULT 1 NOT NULL, quantity NUMERIC DEFAULT 1 NOT NULL,
is_purchased BOOLEAN DEFAULT false NOT NULL, is_purchased BOOLEAN DEFAULT false NOT NULL,
@@ -359,7 +359,7 @@ CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_with_user_id ON public.s
CREATE TABLE IF NOT EXISTS public.suggested_corrections ( CREATE TABLE IF NOT EXISTS public.suggested_corrections (
suggested_correction_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, suggested_correction_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(flyer_item_id) ON DELETE CASCADE, flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(flyer_item_id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.users(user_id), user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE,
correction_type TEXT NOT NULL, correction_type TEXT NOT NULL,
suggested_value TEXT NOT NULL, suggested_value TEXT NOT NULL,
status TEXT DEFAULT 'pending' NOT NULL, status TEXT DEFAULT 'pending' NOT NULL,
@@ -379,9 +379,9 @@ CREATE INDEX IF NOT EXISTS idx_suggested_corrections_pending ON public.suggested
-- 21. For prices submitted directly by users from in-store. -- 21. For prices submitted directly by users from in-store.
CREATE TABLE IF NOT EXISTS public.user_submitted_prices ( CREATE TABLE IF NOT EXISTS public.user_submitted_prices (
user_submitted_price_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_submitted_price_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id UUID NOT NULL REFERENCES public.users(user_id), user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE,
master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
store_id BIGINT NOT NULL REFERENCES public.stores(store_id), store_id BIGINT NOT NULL REFERENCES public.stores(store_id) ON DELETE CASCADE,
price_in_cents INTEGER NOT NULL, price_in_cents INTEGER NOT NULL,
photo_url TEXT, photo_url TEXT,
upvotes INTEGER DEFAULT 0 NOT NULL, upvotes INTEGER DEFAULT 0 NOT NULL,
@@ -424,8 +424,8 @@ COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (
-- 24. For specific products, linking a master item with a brand and size. -- 24. For specific products, linking a master item with a brand and size.
CREATE TABLE IF NOT EXISTS public.products ( CREATE TABLE IF NOT EXISTS public.products (
product_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, product_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
brand_id BIGINT REFERENCES public.brands(brand_id), brand_id BIGINT REFERENCES public.brands(brand_id) ON DELETE SET NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
size TEXT, size TEXT,
@@ -496,7 +496,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_recipes_unique_system_recipe_name ON publi
CREATE TABLE IF NOT EXISTS public.recipe_ingredients ( CREATE TABLE IF NOT EXISTS public.recipe_ingredients (
recipe_ingredient_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_ingredient_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
recipe_id BIGINT NOT NULL REFERENCES public.recipes(recipe_id) ON DELETE CASCADE, recipe_id BIGINT NOT NULL REFERENCES public.recipes(recipe_id) ON DELETE CASCADE,
master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
quantity NUMERIC NOT NULL, quantity NUMERIC NOT NULL,
unit TEXT NOT NULL, unit TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
@@ -780,7 +780,7 @@ CREATE INDEX IF NOT EXISTS idx_shopping_trips_shopping_list_id ON public.shoppin
CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( CREATE TABLE IF NOT EXISTS public.shopping_trip_items (
shopping_trip_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_trip_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(shopping_trip_id) ON DELETE CASCADE, shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(shopping_trip_id) ON DELETE CASCADE,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE SET NULL,
custom_item_name TEXT, custom_item_name TEXT,
quantity NUMERIC NOT NULL, quantity NUMERIC NOT NULL,
price_paid_cents INTEGER, price_paid_cents INTEGER,
@@ -844,7 +844,7 @@ CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(
CREATE TABLE IF NOT EXISTS public.receipts ( CREATE TABLE IF NOT EXISTS public.receipts (
receipt_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, receipt_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE,
store_id BIGINT REFERENCES public.stores(store_id), store_id BIGINT REFERENCES public.stores(store_id) ON DELETE CASCADE,
receipt_image_url TEXT NOT NULL, receipt_image_url TEXT NOT NULL,
transaction_date TIMESTAMPTZ, transaction_date TIMESTAMPTZ,
total_amount_cents INTEGER, total_amount_cents INTEGER,
@@ -865,8 +865,8 @@ CREATE TABLE IF NOT EXISTS public.receipt_items (
raw_item_description TEXT NOT NULL, raw_item_description TEXT NOT NULL,
quantity NUMERIC DEFAULT 1 NOT NULL, quantity NUMERIC DEFAULT 1 NOT NULL,
price_paid_cents INTEGER NOT NULL, price_paid_cents INTEGER NOT NULL,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE SET NULL,
product_id BIGINT REFERENCES public.products(product_id), product_id BIGINT REFERENCES public.products(product_id) ON DELETE SET NULL,
status TEXT DEFAULT 'unmatched' NOT NULL CHECK (status IN ('unmatched', 'matched', 'needs_review', 'ignored')), status TEXT DEFAULT 'unmatched' NOT NULL CHECK (status IN ('unmatched', 'matched', 'needs_review', 'ignored')),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL

View File

@@ -126,8 +126,8 @@ CREATE TABLE IF NOT EXISTS public.flyers (
file_name TEXT NOT NULL, file_name TEXT NOT NULL,
image_url TEXT NOT NULL, image_url TEXT NOT NULL,
icon_url TEXT, icon_url TEXT,
checksum TEXT UNIQUE, checksum TEXT UNIQUE,
store_id BIGINT REFERENCES public.stores(store_id), store_id BIGINT REFERENCES public.stores(store_id) ON DELETE CASCADE,
valid_from DATE, valid_from DATE,
valid_to DATE, valid_to DATE,
store_address TEXT, store_address TEXT,
@@ -155,7 +155,7 @@ CREATE INDEX IF NOT EXISTS idx_flyers_valid_to_file_name ON public.flyers (valid
CREATE TABLE IF NOT EXISTS public.master_grocery_items ( CREATE TABLE IF NOT EXISTS public.master_grocery_items (
master_grocery_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, master_grocery_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
category_id BIGINT REFERENCES public.categories(category_id), category_id BIGINT REFERENCES public.categories(category_id) ON DELETE SET NULL,
is_allergen BOOLEAN DEFAULT false, is_allergen BOOLEAN DEFAULT false,
allergy_info JSONB, allergy_info JSONB,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
@@ -187,13 +187,13 @@ CREATE TABLE IF NOT EXISTS public.flyer_items (
price_in_cents INTEGER, price_in_cents INTEGER,
quantity_num NUMERIC, quantity_num NUMERIC,
quantity TEXT NOT NULL, quantity TEXT NOT NULL,
category_id BIGINT REFERENCES public.categories(category_id), category_id BIGINT REFERENCES public.categories(category_id) ON DELETE SET NULL,
category_name TEXT, category_name TEXT,
unit_price JSONB, unit_price JSONB,
view_count INTEGER DEFAULT 0 NOT NULL, view_count INTEGER DEFAULT 0 NOT NULL,
click_count INTEGER DEFAULT 0 NOT NULL, click_count INTEGER DEFAULT 0 NOT NULL,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE SET NULL,
product_id BIGINT, product_id BIGINT REFERENCES public.products(product_id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
); );
@@ -310,7 +310,7 @@ CREATE INDEX IF NOT EXISTS idx_shopping_lists_user_id ON public.shopping_lists(u
CREATE TABLE IF NOT EXISTS public.shopping_list_items ( CREATE TABLE IF NOT EXISTS public.shopping_list_items (
shopping_list_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_list_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(shopping_list_id) ON DELETE CASCADE, shopping_list_id BIGINT NOT NULL REFERENCES public.shopping_lists(shopping_list_id) ON DELETE CASCADE,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
custom_item_name TEXT, custom_item_name TEXT,
quantity NUMERIC DEFAULT 1 NOT NULL, quantity NUMERIC DEFAULT 1 NOT NULL,
is_purchased BOOLEAN DEFAULT false NOT NULL, is_purchased BOOLEAN DEFAULT false NOT NULL,
@@ -375,7 +375,7 @@ CREATE INDEX IF NOT EXISTS idx_shared_menu_plans_shared_with_user_id ON public.s
CREATE TABLE IF NOT EXISTS public.suggested_corrections ( CREATE TABLE IF NOT EXISTS public.suggested_corrections (
suggested_correction_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, suggested_correction_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(flyer_item_id) ON DELETE CASCADE, flyer_item_id BIGINT NOT NULL REFERENCES public.flyer_items(flyer_item_id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.users(user_id), user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE,
correction_type TEXT NOT NULL, correction_type TEXT NOT NULL,
suggested_value TEXT NOT NULL, suggested_value TEXT NOT NULL,
status TEXT DEFAULT 'pending' NOT NULL, status TEXT DEFAULT 'pending' NOT NULL,
@@ -395,9 +395,9 @@ CREATE INDEX IF NOT EXISTS idx_suggested_corrections_pending ON public.suggested
-- 21. For prices submitted directly by users from in-store. -- 21. For prices submitted directly by users from in-store.
CREATE TABLE IF NOT EXISTS public.user_submitted_prices ( CREATE TABLE IF NOT EXISTS public.user_submitted_prices (
user_submitted_price_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, user_submitted_price_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id UUID NOT NULL REFERENCES public.users(user_id), user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE,
master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
store_id BIGINT NOT NULL REFERENCES public.stores(store_id), store_id BIGINT NOT NULL REFERENCES public.stores(store_id) ON DELETE CASCADE,
price_in_cents INTEGER NOT NULL, price_in_cents INTEGER NOT NULL,
photo_url TEXT, photo_url TEXT,
upvotes INTEGER DEFAULT 0 NOT NULL, upvotes INTEGER DEFAULT 0 NOT NULL,
@@ -439,8 +439,8 @@ COMMENT ON COLUMN public.brands.store_id IS 'If this is a store-specific brand (
-- 24. For specific products, linking a master item with a brand and size. -- 24. For specific products, linking a master item with a brand and size.
CREATE TABLE IF NOT EXISTS public.products ( CREATE TABLE IF NOT EXISTS public.products (
product_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, product_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
brand_id BIGINT REFERENCES public.brands(brand_id), brand_id BIGINT REFERENCES public.brands(brand_id) ON DELETE SET NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
size TEXT, size TEXT,
@@ -510,7 +510,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_recipes_unique_system_recipe_name ON publi
CREATE TABLE IF NOT EXISTS public.recipe_ingredients ( CREATE TABLE IF NOT EXISTS public.recipe_ingredients (
recipe_ingredient_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, recipe_ingredient_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
recipe_id BIGINT NOT NULL REFERENCES public.recipes(recipe_id) ON DELETE CASCADE, recipe_id BIGINT NOT NULL REFERENCES public.recipes(recipe_id) ON DELETE CASCADE,
master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT NOT NULL REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE CASCADE,
quantity NUMERIC NOT NULL, quantity NUMERIC NOT NULL,
unit TEXT NOT NULL, unit TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
@@ -796,7 +796,7 @@ CREATE INDEX IF NOT EXISTS idx_shopping_trips_shopping_list_id ON public.shoppin
CREATE TABLE IF NOT EXISTS public.shopping_trip_items ( CREATE TABLE IF NOT EXISTS public.shopping_trip_items (
shopping_trip_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, shopping_trip_item_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(shopping_trip_id) ON DELETE CASCADE, shopping_trip_id BIGINT NOT NULL REFERENCES public.shopping_trips(shopping_trip_id) ON DELETE CASCADE,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE SET NULL,
custom_item_name TEXT, custom_item_name TEXT,
quantity NUMERIC NOT NULL, quantity NUMERIC NOT NULL,
price_paid_cents INTEGER, price_paid_cents INTEGER,
@@ -862,7 +862,7 @@ CREATE INDEX IF NOT EXISTS idx_user_follows_following_id ON public.user_follows(
CREATE TABLE IF NOT EXISTS public.receipts ( CREATE TABLE IF NOT EXISTS public.receipts (
receipt_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, receipt_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES public.users(user_id) ON DELETE CASCADE,
store_id BIGINT REFERENCES public.stores(store_id), store_id BIGINT REFERENCES public.stores(store_id) ON DELETE CASCADE,
receipt_image_url TEXT NOT NULL, receipt_image_url TEXT NOT NULL,
transaction_date TIMESTAMPTZ, transaction_date TIMESTAMPTZ,
total_amount_cents INTEGER, total_amount_cents INTEGER,
@@ -883,8 +883,8 @@ CREATE TABLE IF NOT EXISTS public.receipt_items (
raw_item_description TEXT NOT NULL, raw_item_description TEXT NOT NULL,
quantity NUMERIC DEFAULT 1 NOT NULL, quantity NUMERIC DEFAULT 1 NOT NULL,
price_paid_cents INTEGER NOT NULL, price_paid_cents INTEGER NOT NULL,
master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id), master_item_id BIGINT REFERENCES public.master_grocery_items(master_grocery_item_id) ON DELETE SET NULL,
product_id BIGINT REFERENCES public.products(product_id), product_id BIGINT REFERENCES public.products(product_id) ON DELETE SET NULL,
status TEXT DEFAULT 'unmatched' NOT NULL CHECK (status IN ('unmatched', 'matched', 'needs_review', 'ignored')), status TEXT DEFAULT 'unmatched' NOT NULL CHECK (status IN ('unmatched', 'matched', 'needs_review', 'ignored')),
created_at TIMESTAMPTZ DEFAULT now() NOT NULL, created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL updated_at TIMESTAMPTZ DEFAULT now() NOT NULL

View File

@@ -620,6 +620,14 @@ describe('AI Routes (/api/ai)', () => {
expect(response.body.text).toContain('server-generated quick insight'); expect(response.body.text).toContain('server-generated quick insight');
}); });
it('POST /quick-insights should accept items with "item" property instead of "name"', async () => {
const response = await supertest(app)
.post('/api/ai/quick-insights')
.send({ items: [{ item: 'test item' }] });
expect(response.status).toBe(200);
});
it('POST /quick-insights should return 500 on a generic error', async () => { it('POST /quick-insights should return 500 on a generic error', async () => {
// To hit the catch block, we can simulate an error by making the logger throw. // To hit the catch block, we can simulate an error by making the logger throw.
vi.mocked(mockLogger.info).mockImplementationOnce(() => { vi.mocked(mockLogger.info).mockImplementationOnce(() => {

View File

@@ -88,10 +88,17 @@ const rescanAreaSchema = z.object({
const flyerItemForAnalysisSchema = z const flyerItemForAnalysisSchema = z
.object({ .object({
name: requiredString('Item name is required.'), item: z.string().nullish(),
// Allow other properties to pass through without validation name: z.string().nullish(),
}) })
.passthrough(); .passthrough()
.refine(
(data) =>
(data.item && data.item.trim().length > 0) || (data.name && data.name.trim().length > 0),
{
message: "Item identifier is required (either 'item' or 'name').",
},
);
const insightsSchema = z.object({ const insightsSchema = z.object({
body: z.object({ body: z.object({

View File

@@ -617,7 +617,9 @@ describe('Auth Routes (/api/auth)', () => {
const setCookieHeader = response.headers['set-cookie']; const setCookieHeader = response.headers['set-cookie'];
expect(setCookieHeader).toBeDefined(); expect(setCookieHeader).toBeDefined();
expect(setCookieHeader[0]).toContain('refreshToken=;'); expect(setCookieHeader[0]).toContain('refreshToken=;');
expect(setCookieHeader[0]).toContain('Expires=Thu, 01 Jan 1970'); // Check for Max-Age=0, which is the modern way to expire a cookie.
// The 'Expires' attribute is a fallback and its exact value can be inconsistent.
expect(setCookieHeader[0]).toContain('Max-Age=0');
}); });
it('should still return 200 OK even if deleting the refresh token from DB fails', async () => { it('should still return 200 OK even if deleting the refresh token from DB fails', async () => {

View File

@@ -98,11 +98,27 @@ describe('AI API Routes Integration Tests', () => {
altitudeAccuracy: null, altitudeAccuracy: null,
heading: null, heading: null,
speed: null, speed: null,
toJSON: () => ({}), toJSON: function () {
return {
latitude: this.latitude,
longitude: this.longitude,
accuracy: this.accuracy,
altitude: this.altitude,
altitudeAccuracy: this.altitudeAccuracy,
heading: this.heading,
speed: this.speed,
};
},
};
const mockStore = {
name: 'Test Store for Trip',
store_id: 1,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
}; };
const response = await aiApiClient.planTripWithMaps( const response = await aiApiClient.planTripWithMaps(
[], [],
undefined, mockStore,
mockLocation, mockLocation,
undefined, undefined,
authToken, authToken,

View File

@@ -60,7 +60,11 @@ describe('Flyer Processing Background Job Integration Test', () => {
// Arrange: Load a mock flyer PDF. // Arrange: Load a mock flyer PDF.
const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg'); const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
const imageBuffer = await fs.readFile(imagePath); const imageBuffer = await fs.readFile(imagePath);
const mockImageFile = new File([imageBuffer], 'test-flyer-image.jpg', { type: 'image/jpeg' }); // Create a unique buffer and filename for each test run to ensure a unique checksum.
// This prevents a 409 Conflict error when the second test runs.
const uniqueContent = Buffer.concat([imageBuffer, Buffer.from(Date.now().toString())]);
const uniqueFileName = `test-flyer-image-${Date.now()}.jpg`;
const mockImageFile = new File([uniqueContent], uniqueFileName, { type: 'image/jpeg' });
const checksum = await generateFileChecksum(mockImageFile); const checksum = await generateFileChecksum(mockImageFile);
// Act 1: Upload the file to start the background job. // Act 1: Upload the file to start the background job.

View File

@@ -54,9 +54,10 @@ describe('Public API Routes Integration Tests', () => {
testFlyer = flyerRes.rows[0]; testFlyer = flyerRes.rows[0];
// Add an item to the flyer // Add an item to the flyer
await pool.query(`INSERT INTO public.flyer_items (flyer_id, item) VALUES ($1, 'Test Item')`, [ await pool.query(
testFlyer.flyer_id, `INSERT INTO public.flyer_items (flyer_id, item, price_display, quantity) VALUES ($1, 'Test Item', '$0.00', 'each')`,
]); [testFlyer.flyer_id],
);
}); });
afterAll(async () => { afterAll(async () => {