Skip to content

Issue #225: Replace self-written address formatter with existing library

Status: ✅ Completed Labels: F: Invoicing, Refactor Milestone: MVP Launch

Overview

Replaced the custom address formatting implementation with @fragaria/address-formatter library, which provides international address formatting based on OpenCage Data's comprehensive address formatting specifications.

Problem Statement

The project had a custom address formatting implementation (AddressFormatters.ts) that only supported two formats: - USA format (house number first) - European format (street name first)

This limited approach didn't account for the diverse address formatting conventions across different countries worldwide.

Solution

Integrated @fragaria/address-formatter (v6.7.1), a JavaScript implementation of OpenCage Data's address formatting specifications that supports almost all international address formats.

Changes Made

1. Package Installation

pnpm add @fragaria/address-formatter

Added to schemas/package.json as a dependency.

2. Code Refactoring

File: schemas/src/AddressFormatters.ts

Before: - Custom formatAddressUSA() function for USA addresses - Custom formatAddressEuropean() function for European addresses - Manual country detection and format selection - ~196 lines of code

After: - Single toAddressFormatterInput() helper to convert PersistedAddress to library format - Uses library's format() function with international templates - Automatic country detection based on ISO country codes - ~141 lines of code - Country code normalization (USA → US) for proper formatting

Key Implementation Details:

// Convert PersistedAddress format to library's expected format
function toAddressFormatterInput(
  address: PersistedAddress,
  countryNameResolver?: (code: string) => string
): Record<string, string | number> {
  // Map our field names to library's expected names
  // street → road, postal_code → postcode, house_number → houseNumber
  const input = {
    road: address.street,
    city: address.city,
    postcode: address.postal_code,
    country: countryName,
    countryCode: normalizedCountryCode,
    // ... conditionally add houseNumber and state
  };

  return input;
}

export function formatPersistedAddress(
  address: PersistedAddress | null | undefined,
  countryNameResolver?: (code: string) => string
): string | undefined {
  const input = toAddressFormatterInput(address, countryNameResolver);

  let formatted = addressFormatter.format(input, {
    appendCountry: true,
  });

  // Normalize output
  formatted = formatted.trimEnd(); // Remove trailing newline
  formatted = formatted.replace("United States of America", "United States");

  // Add note if present
  if (address.note) {
    formatted += `\n${address.note}`;
  }

  return formatted;
}

3. Test Updates

File: schemas/src/AddressFormatters.test.ts

Updated test expectations to match the library's international formatting standards:

  1. Austrian addresses: Library doesn't include state on a separate line (per international standards)
  2. USA without state: Library includes comma between city and postal code
  3. Trailing newlines: Removed from expected values (now trimmed)

All 26 tests passing ✅

Benefits

  1. International Coverage: Supports address formats for almost all countries worldwide
  2. Standards-Based: Uses OpenCage Data's well-researched formatting rules
  3. Maintainability: Formatting rules maintained by the library, not custom code
  4. Reduced Code: Simplified from ~196 to ~141 lines
  5. Better Accuracy: Handles edge cases and regional variations automatically

Library Features Used

  • Automatic format detection based on country code
  • Multi-line formatting for invoices and documents
  • Single-line formatting for compact displays
  • Country name resolution for internationalization
  • Optional field handling (house number, state, notes)

Backwards Compatibility

The refactoring maintains the same public API:

formatPersistedAddress(address, countryNameResolver?) → string | undefined
formatPersistedAddressSingleLine(address, countryNameResolver?) → string | undefined

Output format changes are minimal and align with international standards: - Austrian addresses no longer show state on separate line (matches international format) - USA addresses without state include comma before postal code (standard USA format)

Testing

Unit Tests

All existing tests updated and passing:

pnpm --filter @meisterbill/schemas test AddressFormatters

Results: 26 tests, all passing ✅

Test Coverage

  • ✅ German addresses
  • ✅ Swiss addresses
  • ✅ Austrian addresses (with state)
  • ✅ USA addresses (various state configurations)
  • ✅ Addresses without house numbers
  • ✅ Addresses with notes
  • ✅ Single-line formatting
  • ✅ Edge cases (null, undefined, empty country)

Example Outputs

German Address:

Waldweg 22
81627 München
Germany

USA Address:

100 Main Street
New York, NY 10001
United States

Swiss Address (Single Line):

Hauptstrasse 123, 8000 Zürich, Switzerland

Technical Notes

Country Code Handling

The library requires standard ISO country codes. We normalize non-standard codes:

if (countryCode === "USA" || countryCode === "UNITED STATES") {
  countryCode = "US";
}

Output Normalization

Two post-processing steps ensure consistency:

  1. Trim trailing newline: Library adds \n at end, we trim it
  2. Normalize country name: "United States of America" → "United States"

Field Mapping

Our PersistedAddress schema maps to library format:

Our Field Library Field
street road
house_number houseNumber
postal_code postcode
city city
state state
country country
- countryCode

Files Modified

  1. schemas/package.json - Added dependency
  2. schemas/src/AddressFormatters.ts - Replaced implementation
  3. schemas/src/AddressFormatters.test.ts - Updated test expectations

Dependencies Added

  • @fragaria/address-formatter v6.7.1
  • Based on OpenCage Data's address formatting templates
  • Supports international address formats
  • ~62 transitive dependencies (includes formatting templates)
  • Original issue: #225
  • Related to invoicing feature (F: Invoicing label)

References

Regression Testing Checklist

When testing this change:

  • [ ] Verify invoice PDF generation shows correct addresses
  • [ ] Test addresses from different countries (US, DE, CH, AT, etc.)
  • [ ] Check customer/service provider address display
  • [ ] Validate email templates with addresses
  • [ ] Test addresses with and without house numbers
  • [ ] Verify addresses with additional notes
  • [ ] Check single-line address display in compact views

Future Enhancements

The library supports additional features we could leverage:

  1. Abbreviation mode: Shorten "Avenue" to "Ave", etc. typescript addressFormatter.format(input, { abbreviate: true })

  2. Custom country override: Force specific formatting typescript addressFormatter.format(input, { countryCode: 'UK' })

  3. Fallback country: Handle invalid country codes typescript addressFormatter.format(input, { fallbackCountryCode: 'US' })

Migration Impact

Risk Level: Low

  • Changes are in shared @meisterbill/schemas package
  • All consuming apps (web, api, app) import through the same interface
  • Test coverage ensures backwards compatibility
  • Format differences are minor and standards-compliant

Deployment Notes: - Rebuild @meisterbill/schemas package first - No database migrations required - No API changes required - Works with existing address data