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:
193
static/shared/js/money.js
Normal file
193
static/shared/js/money.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user