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
20 changes: 18 additions & 2 deletions backend/crates/ontology/canonical-adapter-postgres/src/company.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,24 @@ impl PgCompanyPort {

// The receipt store, and with it the tenant-global command-id namespace
// this port shares with every other receipt owner.
// Attribute the receipt to the object whose action it records.
//
// DERIVED from the command's own query, which already implements
// `dispatch_target()` -- the same value the projected-dispatch path uses.
// NOT from `action_key`: that is "unique only per object type" (a bare
// "revise"), so it cannot name a target on its own, and the internal
// reassign path carries "internal.reassign_org_unit", which names none at
// all. The query knows; the string does not.
//
// Without this the row takes the `owner` DEFAULT of 'ontology.action',
// filing a canonical receipt under the pre-existing instance-action path
// -- a wrong attribution recorded as fact.
let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());
sqlx::query(
"INSERT INTO ont_action_command_receipts \
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at, owner, target) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind(org)
.bind(command_uuid)
Expand All @@ -259,6 +273,8 @@ impl PgCompanyPort {
.bind(&command.action_key)
.bind(command.object_type_id)
.bind(created_at)
.bind(receipt_owner.as_str())
.bind(receipt_target.as_str())
.execute(&mut *tx)
.await?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,10 +700,24 @@ pub async fn write_in_tx(

// The receipt store, and with it the tenant-global command-id namespace
// this port shares with every other receipt owner.
// Attribute the receipt to the object whose action it records.
//
// DERIVED from the command's own query, which already implements
// `dispatch_target()` -- the same value the projected-dispatch path uses.
// NOT from `action_key`: that is "unique only per object type" (a bare
// "revise"), so it cannot name a target on its own, and the internal
// reassign path carries "internal.reassign_org_unit", which names none at
// all. The query knows; the string does not.
//
// Without this the row takes the `owner` DEFAULT of 'ontology.action',
// filing a canonical receipt under the pre-existing instance-action path
// -- a wrong attribution recorded as fact.
let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());
sqlx::query(
"INSERT INTO ont_action_command_receipts \
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at, owner, target) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind(org)
.bind(command_uuid)
Expand All @@ -713,6 +727,8 @@ pub async fn write_in_tx(
.bind(&command.action_key)
.bind(command.object_type_id)
.bind(created_at)
.bind(receipt_owner.as_str())
.bind(receipt_target.as_str())
.execute(tx.as_mut())
.await?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,10 +444,24 @@ impl PgJobPositionPort {

// The receipt store, and with it the tenant-global command-id
// namespace this port shares with every other receipt owner.
// Attribute the receipt to the object whose action it records.
//
// DERIVED from the command's own query, which already implements
// `dispatch_target()` -- the same value the projected-dispatch path uses.
// NOT from `action_key`: that is "unique only per object type" (a bare
// "revise"), so it cannot name a target on its own, and the internal
// reassign path carries "internal.reassign_org_unit", which names none at
// all. The query knows; the string does not.
//
// Without this the row takes the `owner` DEFAULT of 'ontology.action',
// filing a canonical receipt under the pre-existing instance-action path
// -- a wrong attribution recorded as fact.
let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());
sqlx::query(
"INSERT INTO ont_action_command_receipts \
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at, owner, target) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind(org)
.bind(command_uuid)
Expand All @@ -457,6 +471,8 @@ impl PgJobPositionPort {
.bind(&command.action_key)
.bind(command.object_type_id)
.bind(created_at)
.bind(receipt_owner.as_str())
.bind(receipt_target.as_str())
.execute(&mut *tx)
.await?;

Expand Down
20 changes: 18 additions & 2 deletions backend/crates/ontology/canonical-adapter-postgres/src/org_unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,24 @@ impl PgOrgUnitPort {

// The receipt store, and with it the tenant-global command-id
// namespace this port shares with every other receipt owner.
// Attribute the receipt to the object whose action it records.
//
// DERIVED from the command's own query, which already implements
// `dispatch_target()` -- the same value the projected-dispatch path uses.
// NOT from `action_key`: that is "unique only per object type" (a bare
// "revise"), so it cannot name a target on its own, and the internal
// reassign path carries "internal.reassign_org_unit", which names none at
// all. The query knows; the string does not.
//
// Without this the row takes the `owner` DEFAULT of 'ontology.action',
// filing a canonical receipt under the pre-existing instance-action path
// -- a wrong attribution recorded as fact.
let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());
sqlx::query(
"INSERT INTO ont_action_command_receipts \
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at, owner, target) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind(org)
.bind(command_uuid)
Expand All @@ -322,6 +336,8 @@ impl PgOrgUnitPort {
.bind(&command.action_key)
.bind(command.object_type_id)
.bind(created_at)
.bind(receipt_owner.as_str())
.bind(receipt_target.as_str())
.execute(&mut *tx)
.await?;

Expand Down
20 changes: 18 additions & 2 deletions backend/crates/ontology/canonical-adapter-postgres/src/person.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,24 @@ impl PgPersonPort {

// The receipt store, and with it the tenant-global command-id
// namespace this port shares with every other receipt owner.
// Attribute the receipt to the object whose action it records.
//
// DERIVED from the command's own query, which already implements
// `dispatch_target()` -- the same value the projected-dispatch path uses.
// NOT from `action_key`: that is "unique only per object type" (a bare
// "revise"), so it cannot name a target on its own, and the internal
// reassign path carries "internal.reassign_org_unit", which names none at
// all. The query knows; the string does not.
//
// Without this the row takes the `owner` DEFAULT of 'ontology.action',
// filing a canonical receipt under the pre-existing instance-action path
// -- a wrong attribution recorded as fact.
let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());
sqlx::query(
"INSERT INTO ont_action_command_receipts \
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at, owner, target) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
)
.bind(org)
.bind(command_uuid)
Expand All @@ -326,6 +340,8 @@ impl PgPersonPort {
.bind(&command.action_key)
.bind(command.object_type_id)
.bind(created_at)
.bind(receipt_owner.as_str())
.bind(receipt_target.as_str())
.execute(&mut *tx)
.await?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -790,3 +790,37 @@ async fn a_stored_receipt_naming_no_dispatch_target_is_refused(owner_pool: PgPoo
"a receipt the roster cannot read must be refused, never replayed; got {refused:?}"
);
}

/// The port records WHOSE receipt this is, not the instance-action default.
///
/// `ont_action_command_receipts.owner` DEFAULTs to `'ontology.action'`, so until
/// the writers passed it explicitly every canonical receipt claimed to belong to
/// the pre-existing instance-action path. A wrong attribution recorded as fact is
/// worse than an absent column, because it reads as an answer.
///
/// The value is derived from the command's own `query.dispatch_target()`, never
/// from `action_key` -- this suite's commands carry `action_key: "revise"`, which
/// is unique only per object type and names no target on its own.
#[sqlx::test(migrations = "../../platform/db/migrations")]
async fn a_company_receipt_is_attributed_to_company(owner_pool: PgPool) {
let (org, actor, port) = fixture(&owner_pool).await;
execute(&port, command(org, actor, revise("주식회사 아크메")))
.await
.expect("the revise must land");

let (owner, target): (String, Option<String>) =
sqlx::query_as("SELECT owner, target FROM ont_action_command_receipts WHERE org_id = $1")
.bind(*org.as_uuid())
.fetch_one(&owner_pool)
.await
.unwrap();
assert_eq!(
owner, "company",
"the receipt must name the object that owns it"
);
assert_eq!(
target.as_deref(),
Some("company.revise"),
"and the dispatch target it records"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,36 @@ async fn a_receipt_written_without_owner_defaults_to_ontology_action(pool: PgPoo
"a defaulted receipt must carry no dispatch target"
);
}

/// A canonical port records its OWN owner and target, not the default.
///
/// `owner` DEFAULTs to `'ontology.action'`, so before the writers passed it
/// explicitly every canonical receipt claimed to belong to the pre-existing
/// instance-action path. That is a wrong attribution recorded as fact — worse
/// than the column not existing, because it reads as an answer.
///
/// Written over `DispatchTarget::ALL` rather than a sample: every dispatch
/// target must be attributable to the object that owns it, so a fourteenth
/// target cannot arrive unattributed.
#[sqlx::test(migrations = "../../platform/db/migrations")]
async fn every_dispatch_target_attributes_to_its_owning_object(pool: PgPool) {
let (org, actor) = seed_actor(&pool).await;
for target in DispatchTarget::ALL {
let owner = ReceiptOwner::Canonical(target.object());
insert_receipt(&pool, org, actor, owner.as_str(), Some(target.as_str()))
.await
.unwrap_or_else(|err| panic!("{target:?} must be storable under {owner:?}: {err}"));
}
let mislabelled: i64 = sqlx::query_scalar(
"SELECT count(*) FROM ont_action_command_receipts \
WHERE org_id = $1 AND owner = 'ontology.action'",
)
.bind(org)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
mislabelled, 0,
"no canonical receipt may fall back to the instance-action default"
);
}
20 changes: 18 additions & 2 deletions backend/crates/payroll/adapter-postgres/src/pay_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,10 +681,24 @@ impl PgPayRunPort {
// this port shares with every other receipt owner. `created_at` is
// supplied rather than defaulted: 0177 declares it `NOT NULL` with NO
// DEFAULT, so an INSERT that omits it is a `23502`.
// Attribute the receipt to the object whose action it records.
//
// DERIVED from the command's own query, which already implements
// `dispatch_target()` -- the same value the projected-dispatch path uses.
// NOT from `action_key`: that is "unique only per object type" (a bare
// "revise"), so it cannot name a target on its own, and the internal
// reassign path carries "internal.reassign_org_unit", which names none at
// all. The query knows; the string does not.
//
// Without this the row takes the `owner` DEFAULT of 'ontology.action',
// filing a canonical receipt under the pre-existing instance-action path
// -- a wrong attribution recorded as fact.
let receipt_target = command.query.dispatch_target();
let receipt_owner = ReceiptOwner::Canonical(receipt_target.object());
let created_at: OffsetDateTime = sqlx::query_scalar(
"INSERT INTO ont_action_command_receipts \
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, now()) RETURNING created_at",
(org_id, command_id, actor_id, payload_digest, receipt, action_key, object_type_id, created_at, owner, target) \
VALUES ($1, $2, $3, $4, $5, $6, $7, now(), $8, $9) RETURNING created_at",
)
.bind(org)
.bind(command_uuid)
Expand All @@ -693,6 +707,8 @@ impl PgPayRunPort {
.bind(&result)
.bind(&command.action_key)
.bind(command.object_type_id)
.bind(receipt_owner.as_str())
.bind(receipt_target.as_str())
.fetch_one(&mut *tx)
.await?;

Expand Down
4 changes: 2 additions & 2 deletions docs/program/executed-tests-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,14 @@
"backend/crates/ontology/adapter-postgres/tests/registry_rls_surfaces_as_runtime_role.rs": 16,
"backend/crates/ontology/application/src/lib.rs": 17,
"backend/crates/ontology/canonical-adapter-postgres/src/lib.rs": 8,
"backend/crates/ontology/canonical-adapter-postgres/tests/company_port_as_runtime_role.rs": 13,
"backend/crates/ontology/canonical-adapter-postgres/tests/company_port_as_runtime_role.rs": 14,
"backend/crates/ontology/canonical-adapter-postgres/tests/employment_port_as_runtime_role.rs": 31,
"backend/crates/ontology/canonical-adapter-postgres/tests/employment_reassign_as_runtime_role.rs": 2,
"backend/crates/ontology/canonical-adapter-postgres/tests/employment_reassign_identity_as_runtime_role.rs": 2,
"backend/crates/ontology/canonical-adapter-postgres/tests/job_position_port_as_runtime_role.rs": 14,
"backend/crates/ontology/canonical-adapter-postgres/tests/org_unit_port_as_runtime_role.rs": 13,
"backend/crates/ontology/canonical-adapter-postgres/tests/person_port_as_runtime_role.rs": 16,
"backend/crates/ontology/canonical-adapter-postgres/tests/receipt_owner_widening.rs": 3,
"backend/crates/ontology/canonical-adapter-postgres/tests/receipt_owner_widening.rs": 4,
"backend/crates/ontology/canonical-domain/src/lib.rs": 11,
"backend/crates/ontology/domain/src/lib.rs": 14,
"backend/crates/ontology/rest/src/lib.rs": 19,
Expand Down
Loading