Inventory management and point-of-sale billing.

Designing the Inventory System

The backend work started with the inventory model: categories, brands, products, and product_variants.

One of the key design decisions was keeping pricing and stock at the variant level rather than directly on the product. A product such as “Axel Detergent” might be available in both 1kg and 5kg packages, with each variant having its own price and stock count.

This decision became particularly important later when connecting the inventory system to the POS billing flow. A sale should not simply reference a product — it needs to identify the exact purchasable variant.

Full CRUD operations were then implemented for categories, brands, products, and variants. These routes are protected by JWT authentication and role-based authorization, ensuring that only authenticated retailers can manage shop inventory.

On the frontend, this work established the foundation for the upcoming inventory management screens, including category and brand management, product information, and variant-level stock.

Building the POS Billing Engine

The biggest feature of the week was the point-of-sale billing workflow.

The backend now supports the lifecycle of a bill: creating a bill, adding products, updating quantities, applying discounts and tax, and eventually completing the transaction.

The frontend turned this into an actual retailer-facing POS interface.

Instead of building one large component, the billing screen was divided into smaller pieces:

  • Bill header
  • Customer selector
  • Product search
  • Item table
  • Quantity controls
  • Totals summary
  • Payment panel

This component-based structure made the interface much easier to modify as requirements evolved.

The frontend initially relied more heavily on local state, but once the real bill-lifecycle endpoints became available, the data layer was rebuilt around the backend. A bill now persists on the server instead of existing only inside the browser.

Making Billing Transaction-Safe

One of the most important backend engineering problems was atomicity.

Completing a sale involves multiple database operations:

  1. Verify that sufficient stock exists.
  2. Decrease inventory for every item.
  3. Record the payment.
  4. Mark the bill as completed.

These operations cannot safely be treated as independent actions.

For example, if stock for the first three products is successfully reduced but the server fails while processing the fourth product, the database could end up with partially updated inventory and an incomplete bill.

The solution was to wrap the entire completion process inside a database transaction.

Either every operation succeeds and commits, or the entire operation rolls back.

This is one of those backend decisions that may not be visible in the UI, but it directly determines whether the POS system can be trusted in a real shop.

Connecting the Frontend to Real APIs

Once the billing APIs became available, the frontend was connected to the real backend endpoints.

Authentication was also wired into the actual API using Axios, including automatic token attachment, validation, and error handling. Login and registration screens were implemented for both retailers and customers, with reusable components such as FormField and toast-based error feedback.

The payment flow was also updated to match the approved design.

Instead of simply displaying payment buttons, the interface now provides a proper Payment Method selection. Selecting a method reveals the relevant receipt actions, including printing, downloading, or sharing the receipt through WhatsApp.

This was a good example of how the UI and API have to evolve together: the frontend needs to represent the backend’s actual transaction state rather than simply simulating a completed sale.

The Hardest Part: Keeping the Contract in Sync

One of the recurring challenges this week was not necessarily writing the frontend or backend code — it was making sure both sides agreed on the same data contract.

Several frontend issues initially looked like UI bugs but were actually API mismatches.

For example, the frontend might expect a field such as name while the backend returns title, or the component might expect a nested variants array that isn’t present in the API response.

The fix was straightforward once identified: compare the component against the actual API response.

But the deeper lesson was more important:

Never build an API-driven UI around assumptions about the response shape.

A real response payload is often more valuable than an hour of guessing.

The Product-Variant Integration Gap

The most important unresolved issue at the end of the week is the relationship between product search and product variants.

The billing API expects a specific product-variant identifier when adding an item to a bill. However, the current product search endpoint returns the product without the variant information required by the billing operation.

This creates a contract mismatch between two backend capabilities:

Product discovery → Product → Variant → Bill item

The frontend cannot reliably solve this by guessing or inventing data. The API needs to expose enough information for the UI to identify the exact variant that the customer is purchasing.

This is a good example of why API design should be driven by the complete business workflow rather than individual endpoints developed in isolation.

Backend Debugging Lessons

Several backend issues also surfaced during the week.

PostgreSQL Type Ambiguity

A raw SQL expression such as $3 * $4 produced a PostgreSQL error because the database could not determine the intended types of the placeholder values.

Adding explicit casts such as:

$3::integer * $4::numeric

resolved the problem.

The lesson was simple: when working directly with SQL, you sometimes need to provide type information that an ORM might normally infer for you.

Migration State

Another issue occurred when an empty migration was executed before its actual SQL was added.

The migration system correctly marked it as applied, so adding SQL later and running the migration again did nothing.

The solution was to roll the migration back first and then execute it again with the correct SQL.

This reinforced an important rule: a migration is not just a file — it is also a recorded state in the database.

Schema Drift

Another particularly useful bug involved application configuration referencing tables such as retailers and api_bills, even though those tables did not exist.

The actual schema used users with a role column and bills.

The migrations themselves were correct. The problem was that application code had drifted away from the real database schema.

This is especially dangerous because incorrect table mappings can compile successfully and only fail when a particular query executes.

Schema-mapping code therefore deserves the same level of discipline as the database migrations themselves.

CORS and Swagger

Swagger’s “Try it out” requests were also being blocked by CORS because the API configuration only allowed the frontend’s origin.

The API’s own Swagger interface was served from another origin, so those requests needed to be allowed as well.

This was another useful reminder that CORS is about the origin of the browser request, not simply whether the application itself is considered trusted.

Product Discovery and Reporting

To support the POS interface, the backend also gained product discovery capabilities.

The product catalog can be searched by name, with results containing information such as category, brand, and current pricing.

A separate category-wise reporting endpoint was added to return products and their variants grouped by category, including variant size, stock, and price.

A barcode field was also introduced into the inventory schema, preparing the system for future barcode-based product lookup.

These APIs form an important bridge between the inventory system and the retailer-facing POS experience.

What This Week Taught Me

The biggest lesson this week was that full-stack development is really contract-driven development.

A database schema affects the API.
The API affects the frontend data model.
The frontend workflow exposes missing backend capabilities.
And business rules such as stock consistency eventually require database-level guarantees.

Working on both sides made these dependencies much more visible.

I also learned that small, focused frontend components pay off quickly. When the payment interface needed to change significantly, the update remained localized because the payment logic was not buried inside a massive billing component.

On the backend, transactional thinking became equally important. A POS system is not just CRUD — operations such as completing a sale have real-world consequences and need strong consistency guarantees.

Finally, the week reinforced the value of verifying instead of assuming. Whether it is an API response, database schema, migration state, or product relationship, guessing may make development feel faster initially, but it usually creates more expensive debugging later.

Looking Ahead

With the core inventory and billing foundations now in place, the next phase is about closing the remaining gaps and making the system more complete.

The immediate priorities are:

  • Resolve the product-to-variant relationship in the billing workflow.
  • Build the retailer inventory management screens.
  • Add category and brand management interfaces.
  • Implement variant-level stock management.
  • Continue improving product search and reporting.
  • Decide how product images should be stored and served.
  • Continue strengthening the POS flow around real-world transaction scenarios.

The most important goal going forward is not simply adding more features, but making sure the database, backend APIs, and frontend experience evolve together as one system.

Author: Muhammad Faisal

Leave a Reply

Your email address will not be published. Required fields are marked *