> ## Documentation Index
> Fetch the complete documentation index at: https://docs-pos.solya.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Return a rental and settle the caution

> IRREVERSIBLE CLOSE-OUT. Takes the unit back, flips the rental to `returned` (terminal — there is no un-return, no re-open and no way to amend a settlement afterwards) and stamps the settlement. Legal from `active` AND from `overdue`; a second call on an already-`returned` rental is 422. `condition` is your assessment: `{"kind":"conforme"}` releases the whole caution, `{"kind":"dommage","retentionCents":N}` retains N cents of it and releases the rest. Only send `dommage` on a damage assessment a human actually made — you are deciding to keep a customer's money. TWO MONEY TRAPS. (1) `retentionCents` is SILENTLY CLAMPED to `depositAmountCents`: ask for more than the caution and you get a 200 with the caution retained in full and no warning, so always compare the returned `settlement.damageRetentionCents` against what you sent. (2) `settlement.lateFeeCents` (`ratePerDayCents x 1.5 x whole days past dueAt`, any part of a day counting as a full one) is REPORTED, not collected: it is NOT deducted from the caution and `releasedDepositCents` is `deposit - retention` alone, so a late `conforme` return releases the entire caution while still owing a fee. Surface a non-zero `lateFeeCents` to the operator — nothing else in this API will charge it. The release/retention itself is executed by the payment terminal against `depositReference`, not by this call. 404 for an unknown id. NOT IDEMPOTENT — there is no dedup key. A SEQUENTIAL replay cannot settle twice (the status guard rejects it), but that guard is an unsynchronised read-then-write with no row lock, so two CONCURRENT returns can both pass it and both settle — never fan this out. If your call times out RE-READ with `getRental` before retrying: a `returned` status means the first attempt already committed, and the settlement you see is the only one there will ever be — never assume a retry produced a fresh one. See solya-pos#956 for the missing `lockKeys`.



## OpenAPI

````yaml /openapi.json post /v1/rentals/{rentalId}/return
openapi: 3.0.3
info:
  title: Solya POS API
  version: 1.0.0
  description: >-
    The Solya POS backend HTTP surface. Every documented operation is
    agent-ready: it carries an `operationId`, an agent-facing `description`, the
    `pos.*` scopes it enforces (`x-required-permissions`) and an `x-agent-tier`.
    Success responses return the payload as raw JSON; failures return the
    `ErrorResponse` envelope (`{ error: { code, message, statusCode } }`).
servers:
  - url: /
    description: The backend, relative to its deployed origin.
security: []
paths:
  /v1/rentals/{rentalId}/return:
    post:
      tags:
        - Rentals
      summary: Return a rental and settle the caution
      description: >-
        IRREVERSIBLE CLOSE-OUT. Takes the unit back, flips the rental to
        `returned` (terminal — there is no un-return, no re-open and no way to
        amend a settlement afterwards) and stamps the settlement. Legal from
        `active` AND from `overdue`; a second call on an already-`returned`
        rental is 422. `condition` is your assessment: `{"kind":"conforme"}`
        releases the whole caution, `{"kind":"dommage","retentionCents":N}`
        retains N cents of it and releases the rest. Only send `dommage` on a
        damage assessment a human actually made — you are deciding to keep a
        customer's money. TWO MONEY TRAPS. (1) `retentionCents` is SILENTLY
        CLAMPED to `depositAmountCents`: ask for more than the caution and you
        get a 200 with the caution retained in full and no warning, so always
        compare the returned `settlement.damageRetentionCents` against what you
        sent. (2) `settlement.lateFeeCents` (`ratePerDayCents x 1.5 x whole days
        past dueAt`, any part of a day counting as a full one) is REPORTED, not
        collected: it is NOT deducted from the caution and
        `releasedDepositCents` is `deposit - retention` alone, so a late
        `conforme` return releases the entire caution while still owing a fee.
        Surface a non-zero `lateFeeCents` to the operator — nothing else in this
        API will charge it. The release/retention itself is executed by the
        payment terminal against `depositReference`, not by this call. 404 for
        an unknown id. NOT IDEMPOTENT — there is no dedup key. A SEQUENTIAL
        replay cannot settle twice (the status guard rejects it), but that guard
        is an unsynchronised read-then-write with no row lock, so two CONCURRENT
        returns can both pass it and both settle — never fan this out. If your
        call times out RE-READ with `getRental` before retrying: a `returned`
        status means the first attempt already committed, and the settlement you
        see is the only one there will ever be — never assume a retry produced a
        fresh one. See solya-pos#956 for the missing `lockKeys`.
      operationId: returnRental
      parameters:
        - schema:
            type: string
            minLength: 1
          in: path
          name: rentalId
          required: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                condition:
                  oneOf:
                    - type: object
                      properties:
                        kind:
                          type: string
                          enum:
                            - conforme
                      required:
                        - kind
                    - type: object
                      properties:
                        kind:
                          type: string
                          enum:
                            - dommage
                        retentionCents:
                          type: integer
                          minimum: 0
                          exclusiveMinimum: true
                          maximum: 9007199254740991
                      required:
                        - kind
                        - retentionCents
              required:
                - condition
              example:
                condition:
                  kind: dommage
                  retentionCents: 1500
      responses:
        '200':
          description: >-
            The closed-out rental: `returned`, with the settlement stamped. In
            this example the unit came back two days late and damaged, so 1500
            cents were retained, 2500 released, and 1920 cents of late fee are
            OWED but not taken.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  skuId:
                    type: string
                  customerId:
                    type: string
                  storeId:
                    type: string
                  period:
                    type: object
                    properties:
                      startAt:
                        type: string
                      dueAt:
                        type: string
                    required:
                      - startAt
                      - dueAt
                    additionalProperties: false
                  days:
                    anyOf:
                      - type: number
                        enum:
                          - 1
                      - type: number
                        enum:
                          - 3
                      - type: number
                        enum:
                          - 7
                      - type: number
                        enum:
                          - 14
                  ratePerDayCents:
                    type: integer
                    minimum: -9007199254740991
                    maximum: 9007199254740991
                  depositAmountCents:
                    type: integer
                    minimum: -9007199254740991
                    maximum: 9007199254740991
                  depositReference:
                    type: string
                  status:
                    type: string
                    enum:
                      - active
                      - returned
                      - overdue
                  settlement:
                    type: object
                    properties:
                      returnedAt:
                        type: string
                      condition:
                        type: string
                        enum:
                          - conforme
                          - dommage
                      lateFeeCents:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                      damageRetentionCents:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                      releasedDepositCents:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                    required:
                      - returnedAt
                      - condition
                      - lateFeeCents
                      - damageRetentionCents
                      - releasedDepositCents
                    additionalProperties: false
                  actorId:
                    type: string
                  rentedAt:
                    type: string
                required:
                  - id
                  - skuId
                  - customerId
                  - storeId
                  - period
                  - days
                  - ratePerDayCents
                  - depositAmountCents
                  - depositReference
                  - status
                  - actorId
                  - rentedAt
                additionalProperties: false
                description: >-
                  The closed-out rental: `returned`, with the settlement
                  stamped. In this example the unit came back two days late and
                  damaged, so 1500 cents were retained, 2500 released, and 1920
                  cents of late fee are OWED but not taken.
                example:
                  id: rnt-2030-000184
                  skuId: >-
                    6b1f0c93a4d7e28150c3af6b9d24e70f8a5c1b3e6d90f27a4c8b5e1d3f7a9c02
                  customerId: cust-1
                  storeId: shop-1
                  period:
                    startAt: '2030-01-01T09:00:00.000Z'
                    dueAt: '2030-01-04T09:00:00.000Z'
                  days: 3
                  ratePerDayCents: 640
                  depositAmountCents: 4000
                  depositReference: PREAUTH-4821
                  status: returned
                  actorId: user-associate-1
                  rentedAt: '2030-01-01T09:00:00.000Z'
                  settlement:
                    returnedAt: '2030-01-06T09:00:00.000Z'
                    condition: dommage
                    lateFeeCents: 1920
                    damageRetentionCents: 1500
                    releasedDepositCents: 2500
        '400':
          description: >-
            The request failed schema validation; `error.fieldErrors` lists the
            fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
        '401':
          description: No valid credential was presented — send a bearer token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The actor is authenticated but lacks the required `pos.*` scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No resource matches the addressed identifier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: The request is well-formed but violates a domain rule.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: An unexpected server error — safe to retry idempotent requests.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - bearerAuth: []
components:
  schemas:
    ValidationErrorResponse:
      type: object
      required:
        - error
      additionalProperties: false
      description: >-
        A `VALIDATION_FAILED` envelope carrying the offending fields in
        `fieldErrors`.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - statusCode
          additionalProperties: false
          properties:
            code:
              type: string
              enum:
                - VALIDATION_FAILED
            message:
              type: string
            statusCode:
              type: integer
            fieldErrors:
              type: array
              description: >-
                One entry per rejected field: the field path and why it was
                rejected.
              items:
                type: object
                required:
                  - field
                  - message
                additionalProperties: false
                properties:
                  field:
                    type: string
                    description: Dot-path of the offending field.
                  message:
                    type: string
                    description: Why the field was rejected.
    ErrorResponse:
      type: object
      required:
        - error
      additionalProperties: false
      description: The uniform failure envelope every non-2xx response returns.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - statusCode
          additionalProperties: false
          properties:
            code:
              type: string
              enum:
                - VALIDATION_FAILED
                - UNAUTHORIZED
                - FORBIDDEN
                - NOT_FOUND
                - CONFLICT
                - BUSINESS_RULE_VIOLATION
                - INTERNAL_ERROR
              description: >-
                Machine-readable kernel `ResultCode` — branch on this, not on
                `message`.
            message:
              type: string
              description: >-
                Human-readable explanation. Safe to surface; never leaks server
                internals.
            statusCode:
              type: integer
              description: >-
                The HTTP status, mirrored into the body so a client need not
                read headers.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        `Authorization: Bearer <token>`. Accepts EITHER a Keycloak access token
        (scopes-in-token) OR an opaque POS session token; both resolve to the
        same `pos.*` scope vocabulary the route guards enforce.

````