Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api/billing/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ def invoice_to_out(
def _format_address(obj: CommunityIdentity | ParticipantContact | None) -> str | None:
if obj is None:
return None
# Coerce each part to str: address components (e.g. a house number) may arrive
# as ints from the CRM, and a TypeError here would dead-letter the whole issue.
# Coerce each part to str. The CRM house number became text on 2026-08-30, but
# this formatter is fed from callers this module does not control, and a
# TypeError here would dead-letter the whole issue.
line1 = " ".join(str(part) for part in (obj.street, obj.number) if part)
line2 = " ".join(str(part) for part in (obj.postcode, obj.city) if part)
joined = ", ".join(str(part) for part in (line1, line2, obj.supplement) if part)
Expand Down
7 changes: 4 additions & 3 deletions ports/crm_core_sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ async def get_community_identity(self, *, id_community: int) -> CommunityIdentit
if row is None:
return None
data = dict(row)
# CRM `address.number` is an integer column; the DTO (and the docgen
# address formatter) treat it as a string. Coerce to honour `str | None`.
# CRM `address.number` is text since 2026-08-30, so this is normally a
# no-op — kept because it is what lets this adapter read a CRM on either
# side of that migration, and the DTO owes `str | None` regardless.
if data.get("number") is not None:
data["number"] = str(data["number"])
return CommunityIdentity(**data)
Expand Down Expand Up @@ -264,7 +265,7 @@ async def participant_contacts(
vat_number=row["company_vat"],
social_rate=bool(row["social_rate"]) if row["social_rate"] is not None else False,
street=row["street"],
# CRM `address.number` is an integer column; DTO expects `str | None`.
# Text since 2026-08-30; kept for the same reason as above.
number=str(row["number"]) if row["number"] is not None else None,
postcode=row["postcode"],
city=row["city"],
Expand Down
4 changes: 2 additions & 2 deletions tests/billing/test_crm_core_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ async def test_get_community_identity(db_session):
db_session, iban="BE68539007547034", legal_name="ACME ASBL", account_holder_name=None
)
addr = await f.create_address(
db_session, id_community=cid, street="Rue de la Loi", number=16, city="Bruxelles"
db_session, id_community=cid, street="Rue de la Loi", number="16", city="Bruxelles"
)
await db_session.execute(
text("UPDATE community SET headquarters_address_id = :a WHERE id = :c"),
Expand Down Expand Up @@ -524,7 +524,7 @@ async def test_community_identity_missing_iban_is_incomplete(db_session):
async def test_participant_contacts_individual_and_company(db_session):
cid = await f.create_community(db_session)
billing_addr = await f.create_address(
db_session, id_community=cid, street="Chaussée de Liège", number=5, city="Namur"
db_session, id_community=cid, street="Chaussée de Liège", number="5", city="Namur"
)
m_ind = await f.create_member(
db_session,
Expand Down
7 changes: 4 additions & 3 deletions tests/billing/test_mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ def test_format_address_normal() -> None:


def test_format_address_tolerates_int_number() -> None:
# Defensive guard: the CRM `address.number` column is an integer, so an int can
# reach the formatter. It must not raise a TypeError — that used to dead-letter
# the whole invoice-issue message and block PDF generation entirely.
# Defensive guard. `address.number` became a VARCHAR(32) on 2026-08-30, so an
# int no longer arrives from the CRM by that route — but the formatter is also
# fed from callers this module does not control, and a TypeError here used to
# dead-letter the whole invoice-issue message and block PDF generation.
assert _format_address(_identity(number=16)) == "Rue de la Loi 16, 1000 Bruxelles"


Expand Down
2 changes: 1 addition & 1 deletion tests/factories/crm_billing_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ async def create_address(
*,
id_community: int,
street: str = "Rue de la Loi",
number: int = 16, # integer column in the real CRM (see crm_test_schema.sql)
number: str = "16", # text column in the real CRM: 12A is a real house number
postcode: str = "1000",
city: str = "Bruxelles",
supplement: str | None = None,
Expand Down
3 changes: 2 additions & 1 deletion tests/sql/crm_test_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ CREATE INDEX IF NOT EXISTS idx_community_subscription_id_community
CREATE TABLE IF NOT EXISTS address (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
street VARCHAR(255),
number INTEGER, -- matches the real CRM: house number is an integer column
number VARCHAR(32), -- matches the real CRM: text, because 12A is a real house number
postcode VARCHAR(16),
supplement VARCHAR(255),
city VARCHAR(255),
country CHAR(2) NOT NULL DEFAULT 'BE',
id_community INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
Expand Down
Loading