{
  "openapi": "3.0.3",
  "info": {
    "title": "NGAIGE Channel API",
    "version": "1.0.0",
    "description": "Versioned product boundary for external integrators: conversation in/out, onboarding, campaigns, dispatch, reporting and the masked STOP read. Consent/STOP/approval-gate are enforced server-side in this contract (refusal shapes pinned). Tenant identity comes from the API key only — no body field, query param or path segment may carry tenant_id. Every mutating POST requires the Idempotency-Key header."
  },
  "servers": [{"url": "/"}],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-tenant API key presented as 'Bearer nk_<key_id>_<secret>'. The id half indexes the store; the secret half is digest-compared in constant time. Keys mint through POST /api/v1/keys (admin:all) and the plaintext is revealed exactly once."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": ["type", "title", "status"],
        "properties": {
          "type": {"type": "string", "description": "RFC7807 type URI; the pinned refusals use urn:ngaige:problem:v1:* types."},
          "title": {"type": "string"},
          "status": {"type": "integer"},
          "detail": {"type": "string"},
          "instance": {"type": "string"}
        }
      },
      "ClientCreateRequest": {
        "type": "object",
        "required": ["business_name", "sender_id"],
        "additionalProperties": false,
        "properties": {
          "business_name": {"type": "string", "description": "Trading name; stored trimmed."},
          "sender_id": {"type": "string", "description": "Handset-visible alphanumeric originator, max 11 chars."}
        }
      },
      "Client": {
        "type": "object",
        "required": ["id", "tenant_id", "business_name", "sender_id", "country", "created_at"],
        "properties": {
          "id": {"type": "string"},
          "tenant_id": {"type": "string", "description": "Key-derived scope; never request-carried."},
          "business_name": {"type": "string"},
          "sender_id": {"type": "string"},
          "country": {"type": "string", "description": "ISO 3166-1 alpha-2 dial context; empty = E.164-only acceptance."},
          "created_at": {"type": "string", "format": "date-time"}
        }
      },
      "ImportRequest": {
        "type": "object",
        "required": ["client_id", "csv"],
        "additionalProperties": false,
        "properties": {
          "client_id": {"type": "string", "description": "Own-tenant client (verified before any write)."},
          "csv": {"type": "string", "description": "CSV text with the exact header msisdn,consent,consented_at."},
          "consent_affirmation": {"type": "boolean", "description": "EXACTLY-ONE proof carrier A: true attests recorded permission exists (rows stamp csv-upload)."},
          "consent_evidence_ref": {"type": "string", "description": "EXACTLY-ONE proof carrier B: a previously saved consent evidence row id."},
          "evidence_captured_at": {"type": "string", "format": "date-time", "description": "Required with consent_evidence_ref: the sheet's capture instant is the consent stamp."}
        }
      },
      "ImportReport": {
        "type": "object",
        "required": ["import_id", "total", "accepted", "rejected_consent", "rejected_format", "duplicates"],
        "properties": {
          "import_id": {"type": "string"},
          "total": {"type": "integer"},
          "accepted": {"type": "integer"},
          "rejected_consent": {"type": "integer"},
          "rejected_format": {"type": "integer"},
          "duplicates": {"type": "integer"}
        }
      },
      "CampaignDraftRequest": {
        "type": "object",
        "required": ["name", "goal", "audience", "message", "schedule"],
        "additionalProperties": false,
        "properties": {
          "name": {"type": "string", "description": "Per-tenant campaign handle; same-name drafts re-answer the stored original (200), never a second row."},
          "goal": {"type": "string"},
          "audience": {"type": "string"},
          "message": {"type": "string"},
          "schedule": {"type": "string", "format": "date-time", "description": "RFC3339; must be strictly in the future."}
        }
      },
      "Campaign": {
        "type": "object",
        "required": ["id", "tenant_id", "name", "goal", "audience", "message", "schedule", "state", "created_at", "updated_at"],
        "properties": {
          "id": {"type": "string"},
          "tenant_id": {"type": "string"},
          "name": {"type": "string"},
          "goal": {"type": "string"},
          "audience": {"type": "string"},
          "message": {"type": "string"},
          "schedule": {"type": "string", "format": "date-time"},
          "state": {"type": "string", "enum": ["DRAFT", "PENDING_APPROVAL", "APPROVED"]},
          "created_at": {"type": "string", "format": "date-time"},
          "updated_at": {"type": "string", "format": "date-time"}
        }
      },
      "PlanEnvelope": {
        "type": "object",
        "required": ["campaign_id", "plan"],
        "properties": {
          "campaign_id": {"type": "string"},
          "plan": {
            "type": "object",
            "description": "The canonical campaign-plan-v1 document; estimates are computed in code.",
            "required": ["schema_version", "goal", "audience", "message", "schedule", "segments", "message_variants", "estimated_messages", "estimated_cost_eur", "generated_at"],
            "properties": {
              "schema_version": {"type": "string", "enum": ["campaign-plan-v1"]},
              "goal": {"type": "string"},
              "audience": {"type": "string"},
              "message": {"type": "string"},
              "schedule": {"type": "string", "format": "date-time"},
              "segments": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["name", "size"],
                  "properties": {
                    "name": {"type": "string"},
                    "size": {"type": "integer", "minimum": 1}
                  }
                }
              },
              "message_variants": {"type": "array", "items": {"type": "string"}},
              "budget_line": {"type": "string"},
              "risk_notes": {"type": "string"},
              "estimated_messages": {"type": "integer"},
              "estimated_cost_eur": {"type": "string", "description": "Decimal string, exactly 4 fractional digits."},
              "generated_at": {"type": "string", "format": "date-time"}
            }
          }
        }
      },
      "CampaignState": {
        "type": "object",
        "required": ["campaign_id", "state"],
        "properties": {
          "campaign_id": {"type": "string"},
          "state": {"type": "string", "enum": ["DRAFT", "PENDING_APPROVAL", "APPROVED"]}
        }
      },
      "ApproveRequest": {
        "type": "object",
        "required": ["actor"],
        "additionalProperties": false,
        "properties": {
          "actor": {"type": "string", "description": "The human approver handle; echoed verbatim into the response and the audit trail. Blank is refused."}
        }
      },
      "Approval": {
        "type": "object",
        "required": ["campaign_id", "state", "approved_by", "approved_at"],
        "properties": {
          "campaign_id": {"type": "string"},
          "state": {"type": "string", "enum": ["APPROVED"]},
          "approved_by": {"type": "string"},
          "approved_at": {"type": "string", "format": "date-time"}
        }
      },
      "DispatchRequest": {
        "type": "object",
        "required": ["campaign_id", "targets"],
        "additionalProperties": false,
        "properties": {
          "campaign_id": {"type": "string", "description": "An APPROVED own-tenant campaign; the approval gate is enforced by the dispatch machine."},
          "targets": {
            "type": "array",
            "minItems": 1,
            "items": {"type": "string", "description": "Recipient msisdn (E.164)."}
          }
        }
      },
      "DispatchReport": {
        "type": "object",
        "required": ["campaign_id", "recipients", "sent", "pending_retry", "terminal_failed", "suppressed", "suppressed_msisdn"],
        "properties": {
          "campaign_id": {"type": "string"},
          "recipients": {"type": "integer"},
          "sent": {"type": "integer"},
          "pending_retry": {"type": "integer"},
          "terminal_failed": {"type": "integer"},
          "suppressed": {"type": "integer"},
          "suppressed_msisdn": {
            "type": "array",
            "items": {"type": "string"},
            "description": "STOP-listed targets in verbatim submission order — a read-only prediction from the same suppression seam; enforcement stays inside the dispatch machine."
          }
        }
      },
      "Report": {
        "type": "object",
        "description": "The campaign report fold (sent/delivered/failed/expired/pending/suppressed + cost block; EUR figures are 4-fractional-digit decimal strings).",
        "required": ["campaign_id", "tenant_id", "campaign_name", "sent", "delivered", "failed", "expired", "pending", "suppressed", "cost"],
        "properties": {
          "campaign_id": {"type": "string"},
          "tenant_id": {"type": "string"},
          "campaign_name": {"type": "string"},
          "sent": {"type": "integer"},
          "delivered": {"type": "integer"},
          "failed": {"type": "integer"},
          "expired": {"type": "integer"},
          "pending": {"type": "integer"},
          "suppressed": {"type": "integer"},
          "cost": {
            "type": "object",
            "required": ["basis", "messages", "rate_eur", "actual_cost_eur", "estimated_cost_eur", "delta_cost_eur", "verdict"],
            "properties": {
              "basis": {"type": "string"},
              "messages": {"type": "integer"},
              "rate_eur": {"type": "string"},
              "actual_cost_eur": {"type": "string"},
              "estimated_cost_eur": {"type": ["string", "null"]},
              "delta_cost_eur": {"type": ["string", "null"]},
              "verdict": {"type": "string"}
            }
          }
        }
      },
      "SuppressionList": {
        "type": "object",
        "required": ["stops", "count"],
        "properties": {
          "stops": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["msisdn_masked", "source_channel", "received_at"],
              "properties": {
                "msisdn_masked": {"type": "string", "description": "Mask law: first 3 + last 3 digits kept, middle digits replaced one-for-one; raw numbers never touch the wire."},
                "source_channel": {"type": "string"},
                "received_at": {"type": "string", "format": "date-time"}
              }
            }
          },
          "count": {"type": "integer", "description": "Always equals len(stops)."}
        }
      },
      "ConversationRequest": {
        "type": "object",
        "required": ["text"],
        "additionalProperties": false,
        "properties": {
          "text": {"type": "string", "description": "The message text; blank is refused."},
          "occurred_at": {"type": "string", "format": "date-time", "description": "Optional; defaults to the service clock."},
          "media": {
            "type": "array",
            "items": {"type": "object"},
            "description": "Known but UNIMPLEMENTED in v1: any element is refused with urn:ngaige:problem:v1:unsupported-media — never a silent text-only fall-through."
          }
        }
      },
      "ConversationReply": {
        "type": "object",
        "required": ["conversation_id", "replies", "handled_at"],
        "properties": {
          "conversation_id": {"type": "string"},
          "replies": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["type", "body"],
              "properties": {
                "type": {"type": "string", "enum": ["text"]},
                "body": {"type": "string"}
              }
            }
          },
          "handled_at": {"type": "string", "format": "date-time"}
        }
      },
      "KeyMintRequest": {
        "type": "object",
        "required": ["scopes"],
        "additionalProperties": false,
        "properties": {
          "scopes": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": ["conversation:write", "onboarding:write", "imports:write", "campaigns:write", "campaigns:approve", "reports:read", "suppression:read", "admin:all"]
            },
            "description": "The closed v1 scope catalogue. admin:all implies every other scope. A scopeless key is refused (a standing hole)."
          }
        }
      },
      "MintedKey": {
        "type": "object",
        "required": ["key_id", "key", "scopes", "created_at"],
        "properties": {
          "key_id": {"type": "string"},
          "key": {"type": "string", "description": "The full presentable key (nk_<key_id>_<secret>): plaintext revealed ONCE here; only its digest is stored."},
          "scopes": {"type": "array", "items": {"type": "string"}},
          "created_at": {"type": "string", "format": "date-time"}
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {"type": "string"},
        "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."
      }
    }
  },
  "paths": {
    "/api/v1/clients": {
      "post": {
        "operationId": "createClient",
        "summary": "Create a client (onboarded SME business).",
        "security": [{"bearerAuth": ["onboarding:write"]}],
        "parameters": [{"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ClientCreateRequest"}}}},
        "responses": {
          "201": {"description": "Client created.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Client"}}}},
          "409": {"description": "Duplicate (tenant, sender_id).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "422": {"description": "Invalid sender id.", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/imports": {
      "post": {
        "operationId": "importRecipients",
        "summary": "Import a consent CSV with EXACTLY ONE consent proof carrier (consent-in-contract).",
        "security": [{"bearerAuth": ["imports:write"]}],
        "parameters": [{"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ImportRequest"}}}},
        "responses": {
          "200": {"description": "Import report.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ImportReport"}}}},
          "404": {"description": "Unknown or foreign client (uniform bytes).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "422": {"description": "No consent proof carrier (urn:ngaige:problem:v1:consent-required), both carriers, or invalid CSV header.", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/campaigns/drafts": {
      "post": {
        "operationId": "createDraft",
        "summary": "Create a campaign DRAFT (same-name re-answer surfaces as 200).",
        "security": [{"bearerAuth": ["campaigns:write"]}],
        "parameters": [{"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CampaignDraftRequest"}}}},
        "responses": {
          "201": {"description": "Draft created.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Campaign"}}}},
          "200": {"description": "Idempotent-by-name re-answer of the stored draft.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Campaign"}}}},
          "422": {"description": "Invalid draft (schedule in the past, missing fields, unparseable schedule).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/campaigns/{id}/plan": {
      "post": {
        "operationId": "generatePlan",
        "summary": "Generate the campaign-plan-v1 document for a DRAFT.",
        "security": [{"bearerAuth": ["campaigns:write"]}],
        "parameters": [
          {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Own-tenant campaign id."},
          {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}
        ],
        "responses": {
          "200": {"description": "Plan envelope.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PlanEnvelope"}}}},
          "404": {"description": "Unknown or foreign campaign (uniform bytes).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "409": {"description": "Invalid state (plan requires DRAFT).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/campaigns/{id}/submit": {
      "post": {
        "operationId": "submitCampaign",
        "summary": "Submit a planned DRAFT for human approval.",
        "security": [{"bearerAuth": ["campaigns:write"]}],
        "parameters": [
          {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
          {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}
        ],
        "responses": {
          "200": {"description": "New state.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CampaignState"}}}},
          "404": {"description": "Unknown or foreign campaign (uniform bytes).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "409": {"description": "Invalid state (submit requires DRAFT with a plan).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/campaigns/{id}/approve": {
      "post": {
        "operationId": "approveCampaign",
        "summary": "Record the human approval decision (own scope: the approval-gate is a consciously delegated power).",
        "security": [{"bearerAuth": ["campaigns:approve"]}],
        "parameters": [
          {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}},
          {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}
        ],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApproveRequest"}}}},
        "responses": {
          "200": {"description": "Approval.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Approval"}}}},
          "404": {"description": "Unknown or foreign campaign (uniform bytes).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "409": {"description": "Invalid state (approve requires PENDING_APPROVAL).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "422": {"description": "Blank actor (an approval names its human).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/dispatch-requests": {
      "post": {
        "operationId": "createDispatchRequest",
        "summary": "Run the gated dispatch for an APPROVED campaign; the response lists STOP-skipped targets (read-only prediction; enforcement stays in the machine).",
        "security": [{"bearerAuth": ["campaigns:write"]}],
        "parameters": [{"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DispatchRequest"}}}},
        "responses": {
          "200": {"description": "Run reconciliation.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DispatchReport"}}}},
          "404": {"description": "Unknown or foreign campaign (uniform bytes).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "409": {"description": "Campaign not approved (urn:ngaige:problem:v1:not-approved).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}},
          "422": {"description": "Missing campaign_id or empty targets.", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/campaigns/{id}/report": {
      "get": {
        "operationId": "getCampaignReport",
        "summary": "The campaign report fold (same fold as every other surface).",
        "security": [{"bearerAuth": ["reports:read"]}],
        "parameters": [
          {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}
        ],
        "responses": {
          "200": {"description": "The folded report.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Report"}}}},
          "404": {"description": "Unknown or foreign campaign (uniform bytes).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/suppression": {
      "get": {
        "operationId": "listSuppression",
        "summary": "The own-tenant STOP ledger, masked (sorted received_at, msisdn ASC).",
        "security": [{"bearerAuth": ["suppression:read"]}],
        "responses": {
          "200": {"description": "Masked stop rows.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SuppressionList"}}}}
        }
      }
    },
    "/api/v1/conversations/{conversation_id}/messages": {
      "post": {
        "operationId": "postConversationMessage",
        "summary": "One synchronous conversation turn (v1-minimal: text only).",
        "security": [{"bearerAuth": ["conversation:write"]}],
        "parameters": [
          {"name": "conversation_id", "in": "path", "required": true, "schema": {"type": "string"}},
          {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}
        ],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ConversationRequest"}}}},
        "responses": {
          "200": {"description": "The turn's replies.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ConversationReply"}}}},
          "422": {"description": "Blank text, or any media element (urn:ngaige:problem:v1:unsupported-media).", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    },
    "/api/v1/keys": {
      "post": {
        "operationId": "mintKey",
        "summary": "Mint a tenant key (admin:all); the plaintext is revealed once.",
        "security": [{"bearerAuth": ["admin:all"]}],
        "parameters": [{"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Required on every mutating POST. Same key + same request fingerprint (method+path+body) replays the stored status+body byte-identically with X-Idempotent-Replay: true; same key + different fingerprint is a 409 urn:ngaige:problem:v1:idempotency-conflict. Ledger scope is (tenant, key)."}],
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KeyMintRequest"}}}},
        "responses": {
          "201": {"description": "The one-shot minted key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MintedKey"}}}},
          "422": {"description": "Empty or unknown scopes.", "content": {"application/problem+json": {"schema": {"$ref": "#/components/schemas/Problem"}}}}
        }
      }
    }
  }
}
