Backend Modules - Mardens-Inc/Pricing-App GitHub Wiki
This document provides a detailed overview of each feature module in the backend.
- Module Overview
- Inventory Module
- Sheets Module
- List Module
- History Module
- POS System Module
- Icons Module
- Server Information Module
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
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)
})Location: src-actix/inventory/
Purpose: Core inventory management functionality including CRUD operations, filtering, real-time updates, and column configuration.
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
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
-
{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
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=50Sorting & Pagination:
?sort_by=date&sort_order=DESC&limit=50&offset=100// 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))
}Location: src-actix/sheets/
Purpose: Excel and CSV file processing for bulk inventory imports.
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
-
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
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
// 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"));
}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"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)
}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)
}Location: src-actix/list/
Purpose: Manage the master list of locations/databases.
list/
├── mod.rs
├── list_data.rs
├── list_db.rs
└── list_endpoint.rs
-
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)
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
);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
Location: src-actix/history/
Purpose: Track all changes across all locations for audit trail.
history/
├── mod.rs
├── history_data.rs
├── history_db.rs
└── history_endpoint.rs
-
GET /api/history/{id}- Get history for a location -
GET /api/history/{id}/{record_id}- Get history for specific record
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
);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?;Location: src-actix/pos_system/
Purpose: Integration with Point of Sale systems via FTP.
pos_system/
├── mod.rs
├── pos_data.rs
├── pos_endpoint.rs
└── ftp_data.rs
-
POST /api/pos/export- Export pricing data to POS -
GET /api/pos/status- Check FTP connection status
// 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(())
}
}Exports inventory data in format expected by POS system (typically CSV).
Location: src-actix/icons_endpoint.rs
Purpose: Manage icons for location databases.
-
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
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)?;- PNG
- JPG/JPEG
- SVG
- GIF
Location: src-actix/server_information_endpoint.rs
Purpose: Provide server version and status information.
-
GET /api/server/version- Get server version -
GET /api/server/health- Health 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(),
}))
}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
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
See Common Tasks for step-by-step guide.
Basic steps:
- Create module directory with 4 files
- Implement
initialize()in*_db.rs - Add endpoints in
*_endpoint.rswith utoipa docs - Register in
lib.rs - Add to
api_doc.rs
- Backend API Design - Learn endpoint patterns
- Backend Testing - Test each module
- Common Tasks - Practical guides
Last Updated: 2025-11-04