---
id: ATS-ACC-BKS-004
scope: CAP-ACC-BKS-004
product_area: accounting
module: books
revision: 2
status: proposed
approved_by: null
approved_at: null
source_sha:
  services-accounting: f47cb263875bbc0c5640fba7dae6785ec32695f8
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
scenarios:
  - id: ATS-ACC-BKS-004-01
    title: Store precise eligible rates
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-01
    preconditions: CAD is supported by the unit.
    steps: >-
      Submit decimal text, numeric input, zero, eleven fractional places, a
      base-currency quote and an unsupported currency.
    expected: Only eligible positive decimal text is stored at the declared precision.
    negative: true
    data_needs: The setup needs no extra fixture records.
    bound_to:
      - TST-ACC-BKS-172
      - TST-ACC-BKS-053
    provenance: test
    source: >-
      services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#refuses
      a number (digits would be lost), zero, a negative, more than ten places,
      nine integer digits and a word
    category: validation
    binding_evidence:
      - >-
        services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#refuses
        a number (digits would be lost), zero, a negative, more than ten places,
        nine integer digits and a word
      - >-
        services-accounting:modules/books/tests/e2e/exchange-rates.e2e.spec.js#refuses
        a rate sent as a number, a malformed day, the base currency and a
        currency the unit is not enabled for
      - >-
        services-accounting:modules/books/tests/e2e/exchange-rates.e2e.spec.js#answers
        200 with the rate to ten places, source manual, version 1
  - id: ATS-ACC-BKS-004-02
    title: Preserve dated rate identity
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-02
      - INV-DOM-ACC-BKS-001-05
    preconditions: A manual CAD quote exists for a date.
    steps: >-
      Duplicate that date and change the existing quote’s date, currency and
      source.
    expected: Every request is refused and the original quote remains.
    negative: true
    data_needs: The setup needs no extra fixture records.
    bound_to:
      - TST-ACC-BKS-084
    provenance: test
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
      a second rate for the currency on the same day
    category: invariant
    binding_evidence:
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
        a second rate for the currency on the same day
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
        changing the unit, the currency or the day: a rate’s identity is
        write-once
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
        changing the write-once source of a manual rate instead of dropping the
        change (DEF-ACT-0010)
  - id: ATS-ACC-BKS-004-03
    title: Resolve quotes consistently
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-03
    preconditions: Two dated CAD quotes exist and a provider-configured EUR has none.
    steps: >-
      Resolve on, between and before quote dates, then compare the database
      function and EUR result.
    expected: >-
      The latest eligible quote and policies agree; earlier and unquoted results
      carry no rate.
    negative: false
    data_needs: The setup needs no extra fixture records.
    bound_to:
      - TST-ACC-BKS-085
      - TST-ACC-BKS-053
    provenance: test
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js#answers
      the latest rate on or before the day with the currency’s settings
    category: rule
    binding_evidence:
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js#answers
        the latest rate on or before the day with the currency’s settings
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js#answers
        the rate of the day itself on its day, and none before the first
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js#is
        what the database function accounting.resolve_exchange_rate answers
        Commerce, in the tenant’s own transaction only
      - >-
        services-accounting:modules/books/tests/e2e/exchange-rates.e2e.spec.js#resolves
        the rate in force on a day, with the currency’s settings, and none for
        EUR with its provider default
  - id: ATS-ACC-BKS-004-04
    title: Fill missing provider quotes
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-05
    preconditions: >-
      Provider currencies include an existing quote and a stubbed partially
      available response.
    steps: Request filling for the date.
    expected: >-
      One provider call saves only missing quotes on provider dates and reports
      skipped and unpriced currencies.
    negative: false
    data_needs: Isolated fixture records matching this scenario’s setup.
    bound_to:
      - TST-ACC-BKS-084
    provenance: test
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#asks
      the provider once for every provider-sourced currency, with the unit, its
      base and the day, and writes provider rows with the platform’s ids
    category: rule
    binding_evidence:
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#asks
        the provider once for every provider-sourced currency, with the unit,
        its base and the day, and writes provider rows with the platform’s ids
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#skips
        a currency whose day already holds a rate, reports one the provider did
        not price, and stores a quote on the provider’s own earlier day
  - id: ATS-ACC-BKS-004-05
    title: Leave configuration intact after fill refusal
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-06
    preconditions: >-
      Cases include manual sourcing, no provider currency, provider
      unavailability and provider decline.
    steps: Request filling in each case.
    expected: Each refusal leaves rates unchanged.
    negative: true
    data_needs: The setup needs no extra fixture records.
    bound_to:
      - TST-ACC-BKS-084
      - TST-ACC-BKS-053
    provenance: test
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
      a currency that takes its rates from the table, one the unit is not
      enabled for, and a unit with no provider currency
    category: failure
    binding_evidence:
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
        a currency that takes its rates from the table, one the unit is not
        enabled for, and a unit with no provider currency
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
        provider_unavailable and provider_declined, writing nothing
      - >-
        services-accounting:modules/books/tests/e2e/exchange-rates.e2e.spec.js#refuses
        the fill 409 provider_unavailable while no platform client exists,
        writing nothing
  - id: ATS-ACC-BKS-004-06
    title: Round conversion at currency precision
    level: unit
    proves:
      - BR-CAP-ACC-BKS-004-07
    preconditions: Quotes cover yen conversion and a credit amount.
    steps: Convert amounts and compute a reciprocal.
    expected: >-
      Minor-unit results use half-away-from-zero rounding; the reciprocal uses
      half-up ten-place rounding.
    negative: false
    data_needs: The setup needs no extra fixture records.
    bound_to:
      - TST-ACC-BKS-172
    provenance: test
    source: >-
      services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#applies
      the target currency’s exponent exactly: 100.00 USD at 149.37 is 14937 yen
    category: boundary
    binding_evidence:
      - >-
        services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#applies
        the target currency’s exponent exactly: 100.00 USD at 149.37 is 14937
        yen
      - >-
        services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#rounds
        half away from zero, for a credit too
      - >-
        services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#converts
        the other way, rounded half up to ten places
  - id: ATS-ACC-BKS-004-07
    title: Correct provider quotes through replacement filling
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-04
    preconditions: A provider quote has been stored.
    steps: Try manual replacement and then deletion.
    expected: Replacement returns a provider-rate conflict; deletion succeeds.
    negative: true
    data_needs: Isolated fixture records matching this scenario’s setup.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/books/services/persistence/exchange-rates.ts:updateExchangeRate
    category: transition
    binding_evidence: []
  - id: ATS-ACC-BKS-004-08
    title: Exercise the grants for maintain dated exchange rates for a unit
    category: permission
    level: api
    proves:
      - BR-CAP-ACC-BKS-016-01
    preconditions: >-
      An application-authorized reader and administrator share the run-owned
      setup. A third principal lacks the relevant resource grant.
    steps: >-
      Read the relevant configuration as the reader, attempt each supported
      mutation, then repeat with the matching administrator grants. Repeat the
      reads without their resource grants.
    expected: >-
      The permission outcomes match the cited authorization rule, and refused
      mutations leave every configured value unchanged.
    negative: true
    data_needs: >-
      Run-owned records for the stated setup; retain the initial values for
      comparison.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/books/tests/e2e/configuration-security.e2e.spec.js#is
      refused the create 403 permission_missing
    permissions_exercised:
      - accounting.exchange-rates.read
      - accounting.exchange-rates.create
      - accounting.exchange-rates.update
      - accounting.exchange-rates.delete
    binding_evidence: []
    supporting_tests: []
  - id: ATS-ACC-BKS-004-09
    title: Quote precision endpoints
    category: boundary
    level: api
    proves:
      - BR-CAP-ACC-BKS-004-01
    preconditions: >-
      An isolated workspace has records needed for both eligible and ineligible
      examples.
    steps: >-
      Create quotes with eight integer and ten fractional digits, then increase
      each length by one.
    expected: Only the eligible precision saves and reads back without losing a digit.
    negative: true
    data_needs: >-
      Run-owned records for the stated setup; retain the initial values for
      comparison.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#refuses
      a number (digits would be lost), zero, a negative, more than ten places,
      nine integer digits and a word
    binding_evidence: []
  - id: ATS-ACC-BKS-004-10
    title: Refuse the second replacement of a shared read version
    category: concurrency
    level: api
    proves:
      - BR-CAP-ACC-BKS-016-05
    preconditions: >-
      Two administrators read the same version of the relevant mutable record.
      Each prepares a different otherwise valid replacement.
    steps: >-
      Release both replacement requests from a barrier. Read the record again
      and retry the losing payload at its original version.
    expected: >-
      One replacement commits, the other is stale, and repeating the stale
      request preserves the winning values.
    negative: true
    data_needs: >-
      Run-owned records for the stated setup; retain the initial values for
      comparison.
    bound_to: []
    provenance: proposed
    source: >-
      services-accounting:modules/books/tests/e2e/payment-terms.e2e.spec.js#answers
      400 naming is_active to a replacement that leaves it out
    interaction_with:
      - CAP-ACC-BKS-016
    binding_evidence: []
  - id: ATS-ACC-BKS-004-11
    title: Unit replacement retains dated quote eligibility
    category: interaction
    level: api
    proves:
      - BR-CAP-ACC-BKS-001-05
      - BR-CAP-ACC-BKS-004-03
    preconditions: >-
      Run-owned records satisfy the preconditions of the referenced capability
      proofs.
    steps: >-
      Save a quote and edit the unit description while omitting its
      supported-set extension, then resolve the quote.
    expected: >-
      The stored quote and currency settings still resolve after the ordinary
      edit.
    negative: false
    data_needs: >-
      Run-owned records for the stated setup; retain the initial values for
      comparison.
    bound_to: []
    provenance: proposed
    source:
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/business-units.integration.spec.js#keeps
        the supported currencies when the replacement does not state them
      - >-
        services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js#answers
        the latest rate on or before the day with the currency’s settings
    exercises:
      - CAP-ACC-BKS-004
      - CAP-ACC-BKS-001
    workflows: []
    binding_evidence: []
evidence:
  - >-
    Verified source file:
    services-accounting:modules/books/services/persistence/exchange-rates.ts
  - >-
    Verified source file:
    services-accounting:modules/books/tests/e2e/exchange-rates.e2e.spec.js
  - >-
    Verified source file:
    services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js
  - >-
    Verified source file:
    services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js
  - >-
    Verified source file:
    services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js
product_intent: proposed
coverage:
  rule: covered
  transition: covered
  validation: covered
  permission: covered
  edge: 'gap: FND-ACC-BKS-065'
  failure: covered
  boundary: covered
  concurrency: 'gap: FND-ACC-BKS-065'
  invariant: covered
  interaction: covered
coverage_notes:
  rule:
    scenarios:
      - ATS-ACC-BKS-004-03
      - ATS-ACC-BKS-004-04
    provenance: documented
    source:
      - ATS-ACC-BKS-004-03
      - ATS-ACC-BKS-004-04
  transition:
    scenarios:
      - ATS-ACC-BKS-004-07
    provenance: documented
    source:
      - ATS-ACC-BKS-004-07
  validation:
    scenarios:
      - ATS-ACC-BKS-004-01
    provenance: documented
    source:
      - ATS-ACC-BKS-004-01
  permission:
    scenarios:
      - ATS-ACC-BKS-004-08
    provenance: documented
    source:
      - ATS-ACC-BKS-004-08
  edge:
    scenarios: []
    provenance: unknown
    source: FND-ACC-BKS-065
    reason: >-
      No scenario isolates this category for the capability at the pinned
      revision.
    gap: FND-ACC-BKS-065
  failure:
    scenarios:
      - ATS-ACC-BKS-004-05
    provenance: documented
    source:
      - ATS-ACC-BKS-004-05
  boundary:
    scenarios:
      - ATS-ACC-BKS-004-06
      - ATS-ACC-BKS-004-09
    provenance: documented
    source:
      - ATS-ACC-BKS-004-06
      - ATS-ACC-BKS-004-09
  concurrency:
    scenarios:
      - ATS-ACC-BKS-004-10
    provenance: unknown
    source: FND-ACC-BKS-065
    gap: FND-ACC-BKS-065
    reason: >-
      The examples isolate part of this category; the finding records its
      remaining specification or execution boundary.
  invariant:
    scenarios:
      - ATS-ACC-BKS-004-02
    provenance: documented
    source:
      - ATS-ACC-BKS-004-02
  interaction:
    scenarios:
      - ATS-ACC-BKS-004-11
    provenance: documented
    source:
      - ATS-ACC-BKS-004-11
rule_coverage:
  - rule: BR-CAP-ACC-BKS-004-01
    scenarios:
      - ATS-ACC-BKS-004-01
      - ATS-ACC-BKS-004-09
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#refuses
      a number (digits would be lost), zero, a negative, more than ten places,
      nine integer digits and a word
  - rule: BR-CAP-ACC-BKS-004-02
    scenarios:
      - ATS-ACC-BKS-004-02
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
      changing the unit, the currency or the day: a rate’s identity is
      write-once
  - rule: BR-CAP-ACC-BKS-004-03
    scenarios:
      - ATS-ACC-BKS-004-03
      - ATS-ACC-BKS-004-11
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.queries.integration.spec.js#answers
      the latest rate on or before the day with the currency’s settings
  - rule: BR-CAP-ACC-BKS-004-04
    scenarios:
      - ATS-ACC-BKS-004-07
    provenance: documented
    source: >-
      services-accounting:modules/books/services/persistence/exchange-rates.ts:updateExchangeRate
  - rule: BR-CAP-ACC-BKS-004-05
    scenarios:
      - ATS-ACC-BKS-004-04
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#skips
      a currency whose day already holds a rate, reports one the provider did
      not price, and stores a quote on the provider’s own earlier day
  - rule: BR-CAP-ACC-BKS-004-06
    scenarios:
      - ATS-ACC-BKS-004-05
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
      provider_unavailable and provider_declined, writing nothing
  - rule: BR-CAP-ACC-BKS-004-07
    scenarios:
      - ATS-ACC-BKS-004-06
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/unit/rules/exchange-rate-rules.spec.js#applies
      the target currency’s exponent exactly: 100.00 USD at 149.37 is 14937 yen
related_coverage:
  - capability: CAP-ACC-BKS-001
    scenarios:
      - ATS-ACC-BKS-004-11
    provenance: documented
    source: >-
      specs/accounting/books/planning/capabilities/CAP-ACC-BKS-004-keep-the-exchange-rates-a-business-unit-prices-and-books.md
workflow_coverage:
  - workflow: WF-ACC-BKS-004
    interactions: []
    provenance: documented
    source: >-
      specs/accounting/books/design/workflows/WF-ACC-BKS-004-fill-missing-exchange-rates-from-the-provider.md
    gap: FND-ACC-BKS-065
coverage_interpretation: >-
  Covered means a specified scenario exists. Proposed scenarios and registered
  bindings do not establish a passed run or verified conformance.
invariant_coverage:
  - invariant: INV-DOM-ACC-BKS-001-01
    scenarios:
      - ATS-ACC-BKS-001-02
    provenance: documented
    source: >-
      services-accounting:modules/books/db/business-units.table.ts:business_units_freeze_trigger
  - invariant: INV-DOM-ACC-BKS-001-02
    scenarios:
      - ATS-ACC-BKS-001-03
      - ATS-ACC-BKS-001-11
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/fiscal-years.integration.spec.js#gives
      the new unit exactly its two years, the one containing today and the next
  - invariant: INV-DOM-ACC-BKS-001-03
    scenarios:
      - ATS-ACC-BKS-002-03
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/e2e/business-units.e2e.spec.js#answers
      400 to a supported currency named twice, in Rhythm 1.0’s words
  - invariant: INV-DOM-ACC-BKS-001-04
    scenarios:
      - ATS-ACC-BKS-002-05
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/e2e/business-units.e2e.spec.js#answers
      409 naming the currency, the unit and the kinds of account that use it
  - invariant: INV-DOM-ACC-BKS-001-05
    scenarios:
      - ATS-ACC-BKS-004-02
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/exchange-rates.integration.spec.js#refuses
      changing the unit, the currency or the day: a rate’s identity is
      write-once
  - invariant: INV-DOM-ACC-BKS-001-06
    scenarios:
      - ATS-ACC-BKS-003-03
      - ATS-ACC-BKS-003-04
    provenance: documented
    source: >-
      services-accounting:modules/books/services/persistence/business-units.ts:assertCardFeeRule;
      services-accounting:modules/books/db/business-units.table.ts:business_units_card_fee_rule_check
  - invariant: INV-DOM-ACC-BKS-001-07
    scenarios:
      - ATS-ACC-BKS-015-04
    provenance: documented
    source: >-
      services-accounting:modules/books/db/business-units.table.ts:business_units_template_references_trigger
  - invariant: INV-DOM-ACC-BKS-001-08
    scenarios:
      - ATS-ACC-BKS-001-06
    provenance: documented
    source: >-
      services-accounting:modules/books/tests/integration/services/persistence/business-units.integration.spec.js#refuses
      a unit with GL, bank and merchant accounts, naming all three
permission_coverage:
  - key: accounting.exchange-rates.read
    where_checked: api
    scenarios:
      - ATS-ACC-BKS-004-08
    provenance: documented
    source: >-
      specs/accounting/books/planning/capabilities/CAP-ACC-BKS-004-keep-the-exchange-rates-a-business-unit-prices-and-books.md
    execution_gap: FND-ACC-BKS-065
  - key: accounting.exchange-rates.create
    where_checked: api
    scenarios:
      - ATS-ACC-BKS-004-08
    provenance: documented
    source: >-
      specs/accounting/books/planning/capabilities/CAP-ACC-BKS-004-keep-the-exchange-rates-a-business-unit-prices-and-books.md
    execution_gap: FND-ACC-BKS-065
  - key: accounting.exchange-rates.update
    where_checked: api
    scenarios:
      - ATS-ACC-BKS-004-08
    provenance: documented
    source: >-
      specs/accounting/books/planning/capabilities/CAP-ACC-BKS-004-keep-the-exchange-rates-a-business-unit-prices-and-books.md
    execution_gap: FND-ACC-BKS-065
  - key: accounting.exchange-rates.delete
    where_checked: api
    scenarios:
      - ATS-ACC-BKS-004-08
    provenance: documented
    source: >-
      specs/accounting/books/planning/capabilities/CAP-ACC-BKS-004-keep-the-exchange-rates-a-business-unit-prices-and-books.md
    execution_gap: FND-ACC-BKS-065
reviewed_at: '2026-10-23'
---

# Maintain dated exchange rates for a unit: Capability Test Specification

## Coverage

- Store precise eligible rates.
- Preserve dated rate identity.
- Resolve quotes consistently.
- Correct provider quotes through replacement filling.
- Fill missing provider quotes.
- Leave configuration intact after fill refusal.
- Round conversion at currency precision.

The structured coverage map distinguishes specified examples from remaining findings. Proposed examples require executable proof before qualification.

## Interactions With Related Capabilities

- Unit replacement retains dated quote eligibility.

Related-capability and workflow references identify the seams each scenario exercises. The lifecycle coverage record retains transitions that still need individual proof.

## Binding Review

The pinned service and browser assertions support the registered bindings. Original test references remain beside each scenario so partial evidence and unresolved conformance remain reviewable.
