Backend Modules - Mardens-Inc/Pricing-App GitHub Wiki

This document provides a detailed overview of each feature module in the backend.

Table of Contents


Module Overview

Each module follows a consistent 4-file pattern:

module/
├── mod.rs               # Public interface
├── module_data.rs       # Data structures
├── module_db.rs         # Database operations
└── module_endpoint.rs   # HTTP handlers

Module Registration

All modules are configured in src-actix/lib.rs:

HttpServer::new(move || {
    App::new()
        .app_data(web::Data::new(pool.clone()))
        .configure(inventory::configure)
        .configure(sheets::configure)
        .configure(list::configure)
        .configure(history::configure)
        .configure(pos_system::configure)
        .configure(icons_endpoint::configure)
        .configure(server_information::configure)
})

Inventory Module

Location: src-actix/inventory/

Purpose: Core inventory management functionality including CRUD operations, filtering, real-time updates, and column configuration.

Structure

inventory/
├── mod.rs
├── inventory_db.rs
├── inventory_endpoint.rs
├── columns/                    # Column configuration submodule
│   ├── mod.rs
│   ├── columns_data.rs
│   ├── columns_db.rs
│   └── columns_endpoint.rs
├── options/                    # Database options submodule
│   ├── mod.rs
│   ├── options_data.rs
│   ├── options_db.rs
│   ├── options_endpoint.rs
│   └── print_options/
│       ├── mod.rs
│       ├── print_options_data.rs
│       └── print_options_db.rs
├── substitutions/              # Item substitutions submodule
│   ├── mod.rs
│   ├── substitution_data.rs
│   ├── substitution_db.rs
│   └── substitution_endpoint.rs
└── foreign_departments/        # Department management submodule
    ├── mod.rs
    ├── foreign_departments_data.rs
    ├── foreign_departments_db.rs
    └── foreign_departments_endpoint.rs

Key Endpoints

Inventory Records:

  • GET /api/inventory/{id} - Get all records with filtering
  • GET /api/inventory/{id}/{record_id} - Get single record
  • POST /api/inventory/{id} - Create new record
  • PUT /api/inventory/{id}/{record_id} - Update record
  • DELETE /api/inventory/{id}/{record_id} - Delete record
  • GET /api/inventory/{id}/updates - SSE stream for real-time updates

Columns:

  • GET /api/inventory/{id}/columns - Get column configuration
  • PUT /api/inventory/{id}/columns - Update column configuration

Options:

  • GET /api/inventory/{id}/options - Get database options
  • PUT /api/inventory/{id}/options - Update database options

Substitutions:

  • GET /api/inventory/{id}/substitutions - Get substitution rules
  • POST /api/inventory/{id}/substitutions - Create substitution
  • DELETE /api/inventory/{id}/substitutions/{sub_id} - Delete substitution

Database Tables

  • {location_id} - Inventory records (dynamic columns)
  • columns_{location_id} - Column configuration
  • options_{location_id} - Database options
  • substitutions_{location_id} - Item substitutions
  • print_options_{location_id} - Print settings

Key Features

Dynamic Schema: Each inventory table has unique columns based on import

Real-time Updates: Uses Server-Sent Events to broadcast changes

broadcast_inventory_update(
    &inventory_id,
    "record_updated",
    &record_id,
    &json!({"data": updated_record})
);

Filtering & Search: Dynamic query building with multiple columns

// Query across multiple columns
?query=shirt&query_columns=description,category&limit=50

Sorting & Pagination:

?sort_by=date&sort_order=DESC&limit=50&offset=100

Usage Example

// inventory_endpoint.rs
#[utoipa::path(
    get,
    path = "/api/inventory/{id}",
    params(
        ("id" = String, Path, description = "Inventory ID"),
        InventoryFilterOptions
    ),
    responses((status = 200, description = "Inventory data"))
)]
pub async fn get_inventory_records(
    pool: web::Data<MySqlPool>,
    id: web::Path<String>,
    options: web::Query<InventoryFilterOptions>,
) -> Result<impl Responder> {
    let inv_id = decode_single(&id)?;
    let result = inventory_db::get_inventory(inv_id, Some(options.into_inner()), &data).await?;
    Ok(web::Json(result))
}

Sheets Module

Location: src-actix/sheets/

Purpose: Excel and CSV file processing for bulk inventory imports.

Structure

sheets/
├── mod.rs
├── spreadsheet_endpoint.rs     # Upload/preview/apply endpoints
├── csv.rs                      # CSV parser
├── excel.rs                    # Excel parser
└── templates/                  # Predefined column mappings
    ├── mod.rs
    └── imperial_template.rs

Key Endpoints

  • POST /api/spreadsheet/prepare - Upload file, get preview
  • POST /api/spreadsheet/apply - Apply spreadsheet to inventory
  • DELETE /api/spreadsheet/ - Clean up temp file
  • GET /api/spreadsheet/sheets - Get sheet names (Excel only)
  • POST /api/spreadsheet/preview-sheet - Preview specific sheet
  • GET /api/spreadsheet/templates - Get predefined templates
  • POST /api/spreadsheet/apply-template - Apply template mapping

File Upload Flow

1. Client uploads file
   ↓
2. POST /api/spreadsheet/prepare
   - Saves file as uploads/{uuid}.xlsx or .csv
   - Returns preview data + identifier
   ↓
3. Client maps columns (or applies template)
   ↓
4. POST /api/spreadsheet/apply
   - Uses X-Identifier header
   - Applies mapped data to inventory
   ↓
5. DELETE /api/spreadsheet/
   - Cleans up temp file

File Handling

// Getting file path with correct extension
let file_path = get_file_path_with_extension(&identifier, &request)?;

// Parses file based on extension
if file_path.ends_with(".csv") {
    csv::parse_csv(&file_path)?
} else if file_path.ends_with(".xlsx") {
    excel::parse_excel(&file_path, sheet_name)?
} else {
    return Err(anyhow!("Unsupported file format"));
}

Template System

Templates provide predefined column mappings for common spreadsheet formats:

// templates/imperial_template.rs
pub struct ImperialTemplate {
    mappings: HashMap<String, String>,
}

// Maps spreadsheet columns to database columns
// Example: "Item Description" → "description"

CSV Parser (csv.rs)

pub fn parse_csv(path: &str) -> Result<Vec<HashMap<String, String>>> {
    let mut reader = csv::Reader::from_path(path)?;
    let headers = reader.headers()?.clone();

    let mut records = Vec::new();
    for result in reader.records() {
        let record = result?;
        let mut map = HashMap::new();
        for (i, field) in record.iter().enumerate() {
            map.insert(headers[i].to_string(), field.to_string());
        }
        records.push(map);
    }

    Ok(records)
}

Excel Parser (excel.rs)

use calamine::{open_workbook, Reader, Xlsx};

pub fn parse_excel(path: &str, sheet: &str) -> Result<Vec<HashMap<String, String>>> {
    let mut workbook: Xlsx<_> = open_workbook(path)?;
    let range = workbook.worksheet_range(sheet)?;

    let mut rows = range.rows();
    let headers = rows.next().ok_or(anyhow!("No headers"))?;

    let mut records = Vec::new();
    for row in rows {
        let mut map = HashMap::new();
        for (i, cell) in row.iter().enumerate() {
            map.insert(
                headers[i].to_string(),
                cell.to_string()
            );
        }
        records.push(map);
    }

    Ok(records)
}

List Module

Location: src-actix/list/

Purpose: Manage the master list of locations/databases.

Structure

list/
├── mod.rs
├── list_data.rs
├── list_db.rs
└── list_endpoint.rs

Key Endpoints

  • GET /api/list - Get all locations
  • GET /api/list/{id} - Get single location
  • POST /api/list - Create new location
  • PUT /api/list/{id} - Update location
  • DELETE /api/list/{id} - Delete location (and all related data)

Database Table

CREATE TABLE locations (
    id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name       VARCHAR(255) NOT NULL,
    location   VARCHAR(255) NOT NULL,
    po         VARCHAR(255) NOT NULL,
    vendor     VARCHAR(255) NOT NULL,
    department VARCHAR(255) NOT NULL,
    image      VARCHAR(255) NOT NULL,
    post_date  DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
    visible    BOOLEAN DEFAULT TRUE NOT NULL
);

Key Features

Visibility Toggle: Locations can be hidden without deletion

UPDATE locations SET visible = false WHERE id = ?

Cascading Delete: Deleting a location removes all related tables

// Delete location and all related tables
pub async fn delete_location(id: u64, pool: &MySqlPool) -> Result<()> {
    // Drop inventory table
    pool.execute(&format!("DROP TABLE IF EXISTS `{}`", id)).await?;

    // Drop related tables
    pool.execute(&format!("DROP TABLE IF EXISTS `columns_{}`", id)).await?;
    pool.execute(&format!("DROP TABLE IF EXISTS `options_{}`", id)).await?;

    // Delete location record
    sqlx::query("DELETE FROM locations WHERE id = ?")
        .bind(id)
        .execute(pool)
        .await?;

    Ok(())
}

Default Values: New locations have default PO, vendor, department for new records


History Module

Location: src-actix/history/

Purpose: Track all changes across all locations for audit trail.

Structure

history/
├── mod.rs
├── history_data.rs
├── history_db.rs
└── history_endpoint.rs

Key Endpoints

  • GET /api/history/{id} - Get history for a location
  • GET /api/history/{id}/{record_id} - Get history for specific record

Database Table

CREATE TABLE history (
    id                 BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    location_id        BIGINT UNSIGNED NOT NULL,
    record_id          INT NOT NULL,
    user_id            BIGINT UNSIGNED NOT NULL,
    action             VARCHAR(50) NOT NULL,
    diff               JSON,
    date               DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
);

Key Features

Change Tracking: Records before/after values

{
    "field_name": {
        "old": "previous value",
        "new": "new value"
    }
}

Action Types:

  • created - New record added
  • updated - Record modified
  • deleted - Record removed

Usage Example:

// Record a change
history_db::create_entry(
    location_id,
    record_id,
    user_id,
    "updated",
    &json!({
        "price": {"old": "10.00", "new": "12.00"},
        "quantity": {"old": "5", "new": "10"}
    }),
    &pool
).await?;

POS System Module

Location: src-actix/pos_system/

Purpose: Integration with Point of Sale systems via FTP.

Structure

pos_system/
├── mod.rs
├── pos_data.rs
├── pos_endpoint.rs
└── ftp_data.rs

Key Endpoints

  • POST /api/pos/export - Export pricing data to POS
  • GET /api/pos/status - Check FTP connection status

FTP Integration

// ftp_data.rs
use ftp::FtpStream;

pub struct FtpClient {
    host: String,
    user: String,
    password: String,
}

impl FtpClient {
    pub fn connect(&self) -> Result<FtpStream> {
        let mut ftp = FtpStream::connect(&self.host)?;
        ftp.login(&self.user, &self.password)?;
        Ok(ftp)
    }

    pub fn upload_file(&self, local_path: &str, remote_path: &str) -> Result<()> {
        let mut ftp = self.connect()?;
        let mut file = std::fs::File::open(local_path)?;
        ftp.put(remote_path, &mut file)?;
        ftp.quit()?;
        Ok(())
    }
}

Data Export Format

Exports inventory data in format expected by POS system (typically CSV).


Icons Module

Location: src-actix/icons_endpoint.rs

Purpose: Manage icons for location databases.

Key Endpoints

  • GET /api/icons - List all icons
  • POST /api/icons - Upload new icon
  • GET /api/icons/{filename} - Get specific icon
  • DELETE /api/icons/{filename} - Delete icon

File Handling

Icons stored in icons/ directory:

const ICONS_FOLDER: &str = "icons";

// Save uploaded icon
let filename = sanitize_filename::sanitize(&user_filename);
let path = format!("{}/{}", ICONS_FOLDER, filename);
std::fs::write(&path, &bytes)?;

Supported Formats

  • PNG
  • JPG/JPEG
  • SVG
  • GIF

Server Information Module

Location: src-actix/server_information_endpoint.rs

Purpose: Provide server version and status information.

Key Endpoints

  • GET /api/server/version - Get server version
  • GET /api/server/health - Health check

Version Check

Frontend compares its version against server version to prompt updates:

#[derive(Serialize)]
pub struct VersionInfo {
    pub version: String,
    pub build_date: String,
}

#[get("/api/server/version")]
pub async fn get_version() -> Result<impl Responder> {
    Ok(web::Json(VersionInfo {
        version: env!("CARGO_PKG_VERSION").to_string(),
        build_date: env!("BUILD_DATE").to_string(),
    }))
}

Module Interactions

Typical Flow for Creating Location

1. List Module: Create location record
   ↓
2. Inventory Module: Create inventory table
   ↓
3. Inventory Module: Create columns table
   ↓
4. Inventory Module: Create options table
   ↓
5. History Module: Log creation event

Typical Flow for Import

1. Sheets Module: Upload spreadsheet
   ↓
2. Sheets Module: Preview and map columns
   ↓
3. Inventory Module: Validate against column config
   ↓
4. Inventory Module: Insert records
   ↓
5. Inventory Module: Broadcast SSE update
   ↓
6. History Module: Log each creation

Adding a New Module

See Common Tasks for step-by-step guide.

Basic steps:

  1. Create module directory with 4 files
  2. Implement initialize() in *_db.rs
  3. Add endpoints in *_endpoint.rs with utoipa docs
  4. Register in lib.rs
  5. Add to api_doc.rs

Next Steps


Last Updated: 2025-11-04

⚠️ **GitHub.com Fallback** ⚠️