feat: integer cents money handling, order page fixes, and vendor filter persistence

Money Handling Architecture:
- Store all monetary values as integer cents (€105.91 = 10591)
- Add app/utils/money.py with Money class and conversion helpers
- Add static/shared/js/money.js for frontend formatting
- Update all database models to use _cents columns (Product, Order, etc.)
- Update CSV processor to convert prices to cents on import
- Add Alembic migration for Float to Integer conversion
- Create .architecture-rules/money.yaml with 7 validation rules
- Add docs/architecture/money-handling.md documentation

Order Details Page Fixes:
- Fix customer name showing 'undefined undefined' - use flat field names
- Fix vendor info empty - add vendor_name/vendor_code to OrderDetailResponse
- Fix shipping address using wrong nested object structure
- Enrich order detail API response with vendor info

Vendor Filter Persistence Fixes:
- Fix orders.js: restoreSavedVendor now sets selectedVendor and filters
- Fix orders.js: init() only loads orders if no saved vendor to restore
- Fix marketplace-letzshop.js: restoreSavedVendor calls selectVendor()
- Fix marketplace-letzshop.js: clearVendorSelection clears TomSelect dropdown
- Align vendor selector placeholder text between pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 20:33:48 +01:00
parent 7f0d32c18d
commit a19c84ea4e
56 changed files with 6155 additions and 447 deletions

193
static/shared/js/money.js Normal file
View File

@@ -0,0 +1,193 @@
// static/shared/js/money.js
/**
* Money handling utilities using integer cents.
*
* All monetary values are stored as integers representing cents in the database.
* The API returns euros (converted from cents on the backend), but these utilities
* can be used if cents are passed to the frontend.
*
* Example:
* 105.91 EUR is stored as 10591 (integer cents)
*
* Usage:
* Money.format(10591) // Returns "105.91"
* Money.format(10591, 'EUR') // Returns "105,91 EUR"
* Money.toCents(105.91) // Returns 10591
* Money.toEuros(10591) // Returns 105.91
* Money.formatEuros(105.91, 'EUR') // Returns "105,91 EUR"
*
* See docs/architecture/money-handling.md for full documentation.
*/
const Money = {
/**
* Format cents as a currency string.
*
* @param {number} cents - Amount in cents
* @param {string} currency - Currency code (default: '', no currency shown)
* @param {string} locale - Locale for formatting (default: 'de-DE')
* @returns {string} Formatted price string
*
* @example
* Money.format(10591) // "105.91"
* Money.format(10591, 'EUR') // "105,91 EUR" (German locale)
* Money.format(1999) // "19.99"
*/
format(cents, currency = '', locale = 'de-DE') {
if (cents === null || cents === undefined) {
cents = 0;
}
const euros = cents / 100;
if (currency) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(euros);
}
return new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(euros);
},
/**
* Convert euros to cents.
*
* @param {number|string} euros - Amount in euros
* @returns {number} Amount in cents (integer)
*
* @example
* Money.toCents(105.91) // 10591
* Money.toCents('19.99') // 1999
* Money.toCents(null) // 0
*/
toCents(euros) {
if (euros === null || euros === undefined || euros === '') {
return 0;
}
return Math.round(parseFloat(euros) * 100);
},
/**
* Convert cents to euros.
*
* @param {number} cents - Amount in cents
* @returns {number} Amount in euros
*
* @example
* Money.toEuros(10591) // 105.91
* Money.toEuros(1999) // 19.99
* Money.toEuros(null) // 0
*/
toEuros(cents) {
if (cents === null || cents === undefined) {
return 0;
}
return cents / 100;
},
/**
* Format a euro amount for display.
*
* Use this when the value is already in euros (e.g., from API response).
*
* @param {number} euros - Amount in euros
* @param {string} currency - Currency code (default: 'EUR')
* @param {string} locale - Locale for formatting (default: 'de-DE')
* @returns {string} Formatted price string
*
* @example
* Money.formatEuros(105.91, 'EUR') // "105,91 EUR"
* Money.formatEuros(19.99) // "19.99"
*/
formatEuros(euros, currency = '', locale = 'de-DE') {
if (euros === null || euros === undefined) {
euros = 0;
}
if (currency) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(euros);
}
return new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(euros);
},
/**
* Parse a price string to cents.
*
* Handles various formats:
* - "19.99 EUR"
* - "19,99"
* - 19.99
*
* @param {string|number} priceStr - Price string or number
* @returns {number} Amount in cents
*
* @example
* Money.parse("19.99 EUR") // 1999
* Money.parse("19,99") // 1999
* Money.parse(19.99) // 1999
*/
parse(priceStr) {
if (priceStr === null || priceStr === undefined || priceStr === '') {
return 0;
}
if (typeof priceStr === 'number') {
return Math.round(priceStr * 100);
}
// Remove currency symbols and spaces
let cleaned = priceStr.toString().replace(/[^\d,.-]/g, '');
// Handle European decimal comma
cleaned = cleaned.replace(',', '.');
try {
return Math.round(parseFloat(cleaned) * 100);
} catch {
return 0;
}
},
/**
* Calculate line total (unit price * quantity).
*
* @param {number} unitPriceCents - Price per unit in cents
* @param {number} quantity - Number of units
* @returns {number} Total in cents
*/
calculateLineTotal(unitPriceCents, quantity) {
return unitPriceCents * quantity;
},
/**
* Calculate order total.
*
* @param {number} subtotalCents - Sum of line items in cents
* @param {number} taxCents - Tax amount in cents (default: 0)
* @param {number} shippingCents - Shipping cost in cents (default: 0)
* @param {number} discountCents - Discount amount in cents (default: 0)
* @returns {number} Total in cents
*/
calculateOrderTotal(subtotalCents, taxCents = 0, shippingCents = 0, discountCents = 0) {
return subtotalCents + taxCents + shippingCents - discountCents;
}
};
// Make available globally
window.Money = Money;
// Export for modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = Money;
}