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
36 changes: 18 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 26 additions & 1 deletion crates/dpp-dal/src/pg/repo_passport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ const PROTECTED_PATCH_FIELDS: [&str; 13] = [
/// transaction. Shared by [`PgPassportRepo::update`] and the transactional
/// outbox's `commit_publish`, so the publish-write and the outbox insert commit
/// atomically without duplicating this SQL. Errors `NotFound` if no row matched.
///
/// # Why `doc || $2` rather than `doc = $2`
///
/// Writing the serialised struct over the whole column erases every stored key
/// the struct does not model. That is not a stale value in memory — it is gone
/// from the database. `update_status` is a read-modify-write (`find_by_id`,
/// mutate, `update`), and publish takes the same path, so the erasure happens on
/// the one write that matters most: the retention guard tests
/// `OLD.retention_locked`, which is still false while the row is a draft, so the
/// guard does not fire, the lossy write lands, and `retention_locked` becomes
/// true in that same statement. Every later write is guarded, so it can never be
/// repaired in place.
///
/// `||` is a shallow merge at the top level: the struct wins on every key it
/// models, and keys it does not model survive. That is exactly the
/// envelope/`sectorData` split — `sectorData` is fully modelled and versioned
/// through the lens chain, so replacing it wholesale is correct; the envelope is
/// the axis with no such mechanism.
///
/// **Constraint this carries:** the `Passport` fields are
/// `skip_serializing_if = "Option::is_none"`, so a field going `Some` -> `None`
/// is absent from `$2` and will no longer clear the stored key. No production
/// path does that today (checked: every envelope-field assignment to `None` is
/// inside a test). A field that genuinely needs clearing must write an explicit
/// JSON `null` rather than rely on omission.
pub(crate) async fn update_passport_in_tx(
tx: &mut Transaction<'_, Postgres>,
passport: &Passport,
Expand All @@ -66,7 +91,7 @@ pub(crate) async fn update_passport_in_tx(
retention_locked = COALESCE(($2->>'retentionLocked')::boolean, retention_locked),
schema_version = COALESCE($2->>'schemaVersion', schema_version),
published_at = COALESCE(NULLIF($2->>'publishedAt','')::timestamptz, published_at),
doc = $2
doc = doc || $2
WHERE id = $1"#,
)
.bind(passport.id.0)
Expand Down
32 changes: 32 additions & 0 deletions crates/dpp-dal/tests/fixtures/passport_docs/aluminium/v1.1.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"id": "019f3aa5-579d-73c1-a3e6-a8002df5e071",
"sector": "aluminium",
"status": "draft",
"batchId": "AL-2026-BATCH-01",
"version": 1,
"createdAt": "2026-08-01T09:00:00.000000000Z",
"materials": [],
"qrCodeUrl": null,
"updatedAt": "2026-08-01T09:00:00.000000000Z",
"sectorData": {
"sector": "aluminium",
"gtin": "09506000134352",
"alloyGrade": "6xxx",
"productionRoute": "primary",
"co2ePerTonneKg": 4200.0,
"recycledContentPct": 15.0,
"countryOfOrigin": "NO"
},
"co2ePerUnit": null,
"productName": "Example Aluminium Extrusion Profile",
"publishedAt": null,
"jwsSignature": null,
"manufacturer": {
"name": "Example Aluminium Works AS",
"address": "NO",
"didWebUrl": null
},
"schemaVersion": "1.1.0",
"retentionLocked": false,
"repairabilityScore": null
}
37 changes: 37 additions & 0 deletions crates/dpp-dal/tests/fixtures/passport_docs/battery/v1.0.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"id": "019f19b0-1111-7342-adb1-d6ab1f410001",
"sector": "battery",
"status": "active",
"batchId": "VB-2026-001",
"version": 1,
"createdAt": "2026-03-04T09:12:44.100000000Z",
"materials": [],
"qrCodeUrl": null,
"updatedAt": "2026-03-04T09:12:44.100000000Z",
"sectorData": {
"gtin": "04901234567009",
"sector": "battery",
"co2ePerUnitKg": 51.4,
"nominalVoltageV": 48.0,
"batteryChemistry": "LFP",
"ratedCapacityKwh": 4.8,
"stateOfHealthPct": null,
"nominalCapacityAh": 100.0,
"expectedLifetimeCycles": 3000,
"recycledContentCobaltPct": null,
"recycledContentNickelPct": null,
"recycledContentLithiumPct": null
},
"co2ePerUnit": { "valueKg": 51.4 },
"productName": "Voltex Industrial Cell 4.8kWh",
"publishedAt": "2026-03-04T09:30:00.000000000Z",
"jwsSignature": null,
"manufacturer": {
"name": "Voltex Battery GmbH",
"address": "DE",
"didWebUrl": null
},
"schemaVersion": "1.0.0",
"retentionLocked": true,
"repairabilityScore": null
}
36 changes: 36 additions & 0 deletions crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.1.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"id": "019f19b0-2222-7342-adb1-d6ab1f410021",
"sector": "battery",
"status": "active",
"batchId": "VB-2026-021",
"version": 1,
"createdAt": "2026-05-11T11:02:07.400000000Z",
"materials": [],
"qrCodeUrl": null,
"updatedAt": "2026-05-11T11:02:07.400000000Z",
"sectorData": {
"gtin": "04901234567009",
"sector": "battery",
"co2ePerUnitKg": 44.9,
"nominalVoltageV": 400.0,
"batteryChemistry": "NMC",
"nominalCapacityAh": 180.0,
"expectedLifetimeCycles": 1500,
"placedOnMarketDate": "2026-05-01",
"carbonFootprintClass": "B",
"carbonFootprintClassRulesetId": "eu-battery-cfb",
"carbonFootprintClassRulesetVersion": "1.0.0"
},
"co2ePerUnit": { "valueKg": 44.9 },
"productName": "Voltex EV Pack 72kWh",
"publishedAt": "2026-05-11T12:00:00.000000000Z",
"jwsSignature": null,
"manufacturer": {
"name": "Voltex Battery GmbH",
"address": "DE",
"didWebUrl": null
},
"schemaVersion": "2.1.0",
"retentionLocked": true,
"repairabilityScore": null
}
37 changes: 37 additions & 0 deletions crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.2.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"id": "019f19b0-3333-7342-adb1-d6ab1f410022",
"sector": "battery",
"status": "active",
"batchId": "VB-2026-022",
"version": 1,
"createdAt": "2026-06-02T14:41:19.900000000Z",
"materials": [],
"qrCodeUrl": null,
"updatedAt": "2026-06-02T14:41:19.900000000Z",
"sectorData": {
"gtin": "04901234567009",
"sector": "battery",
"co2ePerUnitKg": 44.9,
"nominalVoltageV": 400.0,
"batteryChemistry": "NMC",
"nominalCapacityAh": 180.0,
"expectedLifetimeCycles": 1500,
"placedOnMarketDate": "2026-06-01",
"stateOfHealth": {
"parameterSet": "electricVehicle",
"socePct": 97.5
}
},
"co2ePerUnit": { "valueKg": 44.9 },
"productName": "Voltex EV Pack 72kWh",
"publishedAt": "2026-06-02T15:10:00.000000000Z",
"jwsSignature": null,
"manufacturer": {
"name": "Voltex Battery GmbH",
"address": "DE",
"didWebUrl": null
},
"schemaVersion": "2.2.0",
"retentionLocked": true,
"repairabilityScore": null
}
36 changes: 36 additions & 0 deletions crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.3.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"id": "019f19b0-4444-7342-adb1-d6ab1f410023",
"sector": "battery",
"status": "active",
"batchId": "VB-2026-023",
"version": 1,
"createdAt": "2026-06-24T08:15:33.200000000Z",
"materials": [],
"qrCodeUrl": null,
"updatedAt": "2026-06-24T08:15:33.200000000Z",
"sectorData": {
"gtin": "04901234567009",
"sector": "battery",
"co2ePerUnitKg": 39.1,
"nominalVoltageV": 48.0,
"batteryChemistry": "LFP",
"nominalCapacityAh": 100.0,
"expectedLifetimeCycles": 3500,
"recycledContentCobaltPct": 16.0,
"recycledContentLithiumPct": 6.0,
"recycledContentNickelPct": 6.0,
"recycledContentReportingYear": 2025
},
"co2ePerUnit": { "valueKg": 39.1 },
"productName": "Voltex Stationary Cell 4.8kWh",
"publishedAt": "2026-06-24T09:00:00.000000000Z",
"jwsSignature": null,
"manufacturer": {
"name": "Voltex Battery GmbH",
"address": "DE",
"didWebUrl": null
},
"schemaVersion": "2.3.0",
"retentionLocked": true,
"repairabilityScore": null
}
52 changes: 52 additions & 0 deletions crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.4.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"id": "019f19b0-5555-7342-adb1-d6ab1f410024",
"sector": "battery",
"status": "active",
"batchId": "VB-2026-024",
"version": 1,
"createdAt": "2026-07-30T16:27:51.700000000Z",
"materials": [],
"qrCodeUrl": null,
"updatedAt": "2026-07-30T16:27:51.700000000Z",
"sectorData": {
"gtin": "04901234567009",
"sector": "battery",
"co2ePerUnitKg": 39.1,
"nominalVoltageV": 48.0,
"batteryChemistry": "LFP",
"nominalCapacityAh": 100.0,
"expectedLifetimeCycles": 3500,
"placedOnMarketDate": "2026-07-15",
"stateOfHealth": {
"parameterSet": "stationaryOrLmt",
"remainingCapacityPct": 98.2,
"remainingPowerCapabilityPct": null,
"remainingRoundTripEfficiencyPct": null,
"selfDischargeRatePctPerMonth": 1.4,
"ohmicResistanceMohm": null
},
"expectedLifetime": {
"putIntoServiceDate": "2026-07-20",
"energyThroughputKwh": 1240.0,
"capacityThroughputAh": 25800.0,
"harmfulEvents": {
"deepDischargeEvents": 2,
"hoursInExtremeTemperature": 11.5,
"hoursChargingInExtremeTemperature": null
},
"fullEquivalentCycles": 258.0
}
},
"co2ePerUnit": { "valueKg": 39.1 },
"productName": "Voltex Stationary Cell 4.8kWh",
"publishedAt": "2026-07-30T17:00:00.000000000Z",
"jwsSignature": null,
"manufacturer": {
"name": "Voltex Battery GmbH",
"address": "DE",
"didWebUrl": null
},
"schemaVersion": "2.4.0",
"retentionLocked": true,
"repairabilityScore": null
}
Loading
Loading