8.7 KiB
Project Handoff
This document summarizes the current state of the spreadsheet importer so it can be copied into a new project or used as a reference for the next stage.
What This App Does
This is a plain PHP and Bootstrap web app that:
- accepts an Excel upload
- validates the workbook headers
- ignores the
Revenue Typecolumn - normalizes and stores imported row data in PostgreSQL
- calculates stage 1 derived fields during import
- wipes previous imported data before each new import
- shows a 20-row preview on the home page
- provides a full DataTables page for the latest imported batch
The app is structured so future import stages can add more rules without rewriting the UI or database layer.
Move Notes
The codebase is largely relocatable because internal includes and asset URLs are built from the project root.
Before moving the project into a new folder:
- set
APP_URLto the new base URL or folder path - keep
DATABASE_URLor the values inconfig.phpaligned with the target environment - rerun
composer installifvendor/is not copied with the project - confirm the web server is still pointing at
public/as the document root
The only folder-sensitive setting in the app itself is the redirect base URL used after imports and clears.
Current Stack
- PHP
- Bootstrap 5
- PostgreSQL
- Composer for the spreadsheet library
- OpenSpout for reading Excel files
Important Files
-
config.php- Root config file for app name, URL, and PostgreSQL connection settings.
- This replaced
.env-style configuration.
-
bootstrap.php- Starts the session.
- Loads Composer's autoloader if present.
- Requires the app files manually.
- Exposes the
app_config()helper for config lookup.
-
index.php- Root entrypoint.
- Loads
public/index.phpso the app can be reached from the project root.
-
public/index.php- Main web entrypoint.
- Creates the controller and renders a friendly setup error page if startup fails.
-
public/assets/app.js- Handles file upload progress.
- Drives the chunked import loop with repeated
process-chunkrequests. - Handles the clear-data button.
- Reloads the page after import or clear completes.
-
public/assets/styles.css- Custom Bootstrap styling and layout.
-
templates/index.php- Home page with upload form, status messages, and a 20-row preview table.
-
templates/table.php- Full-table page for the latest imported batch.
- DataTables server-side search and paging.
- ColumnControl header buttons and header filters.
-
templates/partials/navbar.php- Shared navigation between the preview and full-table pages.
-
bin/migrate.php- Creates the PostgreSQL schema.
-
app/Database.php- Builds the PDO connection from
config.php.
- Builds the PDO connection from
-
app/Http/AppController.php- Coordinates upload, clear, render, and JSON responses.
-
app/Import/SpreadsheetReader.php- Streams workbook data with OpenSpout.
- Builds a temporary chunk cache from the uploaded file.
- Detects the real header row.
- Ignores
Revenue Type. - Normalizes cell values.
-
app/Import/SpreadsheetRowProcessor.php- Applies all stage 1 calculated field rules.
-
app/Import/ImportService.php- Orchestrates validation, clearing, processing, and storing.
-
app/Import/ImportRepository.php- Handles PostgreSQL inserts, truncation, counting, and pagination.
Database Design
The schema is intentionally flexible and uses JSONB for future growth.
import_batches
Stores metadata for each import run:
- source filename
- original headers
- calculated headers
- row count
- warning count
- created timestamp
import_rows
Stores each imported row:
- batch id
- source row number
- original row data as JSONB
- normalized row data as JSONB
- calculated row data as JSONB
- merged row data as JSONB
The row-level JSONB storage makes it easier to add more calculated fields and import stages later without changing the schema every time.
Import Flow
The current flow is:
- User uploads an Excel workbook.
- The app validates the file extension.
- The upload is stored in a temp file and an import token is returned.
- The workbook is streamed once into a temporary chunk cache.
- The browser repeatedly calls
action=process-chunkwith that token. - Required columns are checked by header name.
- Existing imported data is wiped.
- A new batch record is created.
- Each cached row is normalized.
- Derived fields are calculated.
- The row is inserted into PostgreSQL.
- The home page reloads and shows a 20-row preview table.
- The full table page provides server-side search and pagination for the latest batch.
Validation Rules
Required columns:
Accrual CategoryLocal Selling Company (S/T)Contracting PartyRights Holder Liable FlagGlobal Transfer Pricing FlagDomestic/ Foreign FlagOwnership Type
The app also:
- handles blank and null values safely
- preserves formatted text values from Excel
- treats numeric-looking values consistently
- shows readable import errors
Stage 1 Calculated Fields
1. Actual/Accrual
Rule:
- If
Accrual Categoryequals##, returnActual - Otherwise return
Accrual
2. US/ex-US sale
Rule:
- If
Local Selling Company (S/T)is between1700and1809inclusive, returnUS - Otherwise return
ex-US
3. Contracting Party Filtered
Rule:
- If
Contracting Partyequals1756, return1756 - If
Contracting Partyequals1100, return1100 - If
Contracting Partyequals1902, return1902 - Otherwise return
LOCAL
4. String
Rule:
Concatenate, in this exact order with no separator:
Contracting Party FilteredRights Holder Liable FlagGlobal Transfer Pricing FlagDomestic/ Foreign FlagOwnership Type
Example:
1756YYFDI
5. 2110 Applicable X201
Rule:
- If the
Stringvalue is in the approved list, returnX - Otherwise return blank
6. Class
Rules are applied top to bottom and the first match wins:
AifContracting Party = 1902BifContracting Party = 1756,Rights Holder Liable Flag = Y, andLocal Selling Company (S/T)is not between1700and1808inclusiveCifContracting Party = 1756,Local Selling Company (S/T)is between1700and1808inclusive, and2110 Applicable X201 = XDifContracting Party = 1756,Local Selling Company (S/T)is between1700and1808inclusive, and2110 Applicable X201is notXEifContracting Party = 1100andLocal Selling Company (S/T)is not between1700and1808inclusiveFifContracting Party = 1100andLocal Selling Company (S/T)is between1700and1808inclusiveGifContracting Party Filtered = LOCALHifContracting Party = 1756,Rights Holder Liable Flag = N,Global Transfer Pricing Flag = Y, andLocal Selling Company (S/T)is not between1700and1808inclusive
UI
The Bootstrap UI includes:
- upload form
- navbar for preview/full-table navigation
- progress bar
- success/error messages
- imported row count
- clear imported data button
- 20-row data preview table
- current batch summary
- full table page with search and pagination
The app uses relative asset paths so it works from either:
- the project root, or
- the
public/directory
How To Set This Up In A New Project
-
Copy these files/folders into the new project:
config.phpbootstrap.phpindex.phpbin/app/public/templates/composer.jsoncomposer.lock
-
Install dependencies:
composer install -
Update
config.phpwith the new server's database settings. -
Run migrations:
composer run migrate -
Configure the web server to point at the project root or
public/. -
Open the app in a browser and upload the workbook.
Notes For Future Stages
- Keep the processing logic out of the template.
- Add new calculated fields in
SpreadsheetRowProcessor. - Add new schema fields only if JSONB is no longer sufficient.
- If later stages need multiple import histories, stop truncating and switch to batch filtering in the UI.
- If the workbook structure changes, update header detection in
SpreadsheetReader.
Current Caveats
- PostgreSQL credentials must be correct in
config.php. - The app currently expects the database to exist before migration.
- The import path uses PhpSpreadsheet, so Composer remains required.
- Large workbooks should continue to use pagination in the preview table.
Working Assumptions
- The header names remain stable.
- The example workbook is representative of the real import structure.
Revenue Typeshould always be ignored.- Future stages will extend the current batch/import model rather than replace it.