{"id":37024548,"url":"https://github.com/tellesy/fcms-client","last_synced_at":"2026-01-14T02:58:22.829Z","repository":{"id":311330788,"uuid":"1043377549","full_name":"Tellesy/fcms-client","owner":"Tellesy","description":"Kotlin/Java SDK for FCMS APIs — fast, lightweight, no Spring.","archived":false,"fork":false,"pushed_at":"2025-12-03T15:33:35.000Z","size":141,"stargazers_count":5,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-06T12:34:41.190Z","etag":null,"topics":["banking","fintech","fintech-api","libya"],"latest_commit_sha":null,"homepage":"https://tellesy.dev","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Tellesy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-23T18:13:38.000Z","updated_at":"2025-12-03T15:33:39.000Z","dependencies_parsed_at":"2025-08-24T08:31:06.463Z","dependency_job_id":"a2034fed-5770-418b-8302-be0feae5e6bb","html_url":"https://github.com/Tellesy/fcms-client","commit_stats":null,"previous_names":["tellesy/fcms-client"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Tellesy/fcms-client","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tellesy%2Ffcms-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tellesy%2Ffcms-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tellesy%2Ffcms-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tellesy%2Ffcms-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Tellesy","download_url":"https://codeload.github.com/Tellesy/fcms-client/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tellesy%2Ffcms-client/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408799,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["banking","fintech","fintech-api","libya"],"created_at":"2026-01-14T02:58:22.165Z","updated_at":"2026-01-14T02:58:22.809Z","avatar_url":"https://github.com/Tellesy.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FCMS Client (Kotlin/Java SDK)\n\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.tellesy/fcms-client.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.tellesy/fcms-client)\n\nA small, dependency-light JVM SDK for FCMS APIs (Salaries, Accounts, Requests). Works in any Kotlin or Java app (no Spring dependency). Optimized for performance and correctness.\n\n- Group: `io.github.tellesy`\n- Artifact: `fcms-client`\n- Version: `1.0.4`\n- JVM: Java 21+\n\n## What's New in 1.0.4\n\n- Entity model: `name` is now nullable (`String?`) to gracefully handle nulls from the API.\n- Verified null-safe deserialization across optional fields (e.g., `bankAccount.bankBranch`, `transaction.description`).\n- No breaking API changes; this is a robustness update.\n\n## Supported Endpoints\n\n- GET `{baseUrl}/api/v1/mof/transactions` → `Page\u003cTransaction\u003e`\n- GET `{baseUrl}/api/v1/mof/transactions/{uuid}` → `Transaction`\n- POST `{baseUrl}/api/v1/mof/transactions/{uuid}/complete` → `Transaction`\n- POST `{baseUrl}/api/v1/mof/transactions/{uuid}/reject` → `Transaction`\n- GET `{baseUrl}/api/v1/misc/mof/rejection-reasons` → `List\u003cRejectionReason\u003e`\n\n- GET `{baseUrl}/api/v1/bank-accounts` → `Page\u003cBankAccount\u003e` (supports filters via `AccountsListFilter`)\n- PATCH `{baseUrl}/api/v1/bank-accounts/{uuid}/match` → `BankAccount`\n- PATCH `{baseUrl}/api/v1/bank-accounts/{uuid}/reject` → `BankAccount`\n- PATCH `{baseUrl}/api/v1/bank-accounts/{uuid}/unreject` → `BankAccount`\n- PATCH `{baseUrl}/api/v1/bank-accounts/{uuid}/update` → `BankAccount`\n\n- GET `{baseUrl}/api/v1/purchase-requests-queue` → `Page\u003cPurchaseRequestQueueItem\u003e`\n\nJSON is automatically unwrapped from envelopes like `{ \"data\": ... }`. Pagination is resilient to both Laravel shapes: root `links` object/array and `meta.links` arrays.\n\n## Add Dependency\n\nGradle (Kotlin DSL):\n```kotlin\nrepositories { mavenCentral() }\ndependencies { implementation(\"io.github.tellesy:fcms-client:1.0.4\") }\n```\n\nMaven:\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003eio.github.tellesy\u003c/groupId\u003e\n  \u003cartifactId\u003efcms-client\u003c/artifactId\u003e\n  \u003cversion\u003e1.0.4\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n## Quick Start\n\nKotlin:\n```kotlin\nimport ly.neptune.nexus.fcms.core.FcmsConfig\nimport ly.neptune.nexus.fcms.core.RequestOptions\nimport ly.neptune.nexus.fcms.salaries.*\nimport ly.neptune.nexus.fcms.salaries.model.request.CompleteTransactionRequest\nimport ly.neptune.nexus.fcms.accounts.*\nimport ly.neptune.nexus.fcms.requests.*\n\nsuspend fun main() {\n    val config = FcmsConfig(\n        baseUrl = System.getenv(\"FCMS_BASE_URL\"),\n        tokenProvider = { System.getenv(\"FCMS_TOKEN\") }\n    )\n\n    val salaries = FcmsSalariesClients.create(config)\n    val accounts = FcmsAccountsClients.create(config)\n    val requests = FcmsRequestsClients.create(config)\n\n    // Salaries\n    val page1 = salaries.listTransactions(page = 1)\n    // Typed filtering\n    val pending2025 = salaries.listTransactionsFiltered(\n        page = 1,\n        filter = SalariesListFilter(state = \"pending\", year = 2025)\n    )\n    val tx = salaries.showTransaction(\n        uuid = \"8bb8fbde-21d7-4a37-99eb-fdce5294a1ee\",\n        options = RequestOptions(\n            baseUrlOverride = \"https://other-bank.example.com\",\n            tokenOverride = \"Bearer \u003cother token\u003e\"\n        )\n    )\n    salaries.completeTransaction(\n        uuid = \"569a715c-1053-4be7-acff-b65a8d915724\",\n        request = CompleteTransactionRequest(\"BANK-REF-123\", \"1724232056\")\n    )\n\n    // Accounts\n    val accountsPage = accounts.listAccounts(\n        page = 1,\n        filter = AccountsListFilter(state = \"pending\")\n    )\n    // Requests (pending purchase requests queue)\n    val queuePage = requests.listPendingRequests(page = 1)\n}\n```\n\nJava:\n```java\nimport ly.neptune.nexus.fcms.core.FcmsConfig;\nimport ly.neptune.nexus.fcms.core.RequestOptions;\nimport ly.neptune.nexus.fcms.salaries.*;\nimport ly.neptune.nexus.fcms.salaries.model.Page;\nimport ly.neptune.nexus.fcms.salaries.model.Transaction;\nimport ly.neptune.nexus.fcms.salaries.model.request.CompleteTransactionRequest;\n\npublic class Example {\n  public static void main(String[] args) throws Exception {\n    FcmsConfig config = new FcmsConfig(\n      System.getenv(\"FCMS_BASE_URL\"),\n      () -\u003e System.getenv(\"FCMS_TOKEN\")\n    );\n    try (FcmsSalariesClientJava client = FcmsSalariesClientJava.create(config)) {\n      Page\u003cTransaction\u003e page = client.listTransactions(1, null).get();\n      // Typed filtering from Java\n      Page\u003cTransaction\u003e filtered = client\n        .listTransactions(1, new SalariesListFilter(\"pending\", 2025, null), null)\n        .get();\n      Transaction t = client.showTransaction(\"8bb8fbde-21d7-4a37-99eb-fdce5294a1ee\", null).get();\n    }\n  }\n}\n```\n\n## Configuration\n\n`FcmsConfig` sets global client defaults. `RequestOptions` allows per-call overrides:\n\n- Base URL: `FcmsConfig.baseUrl` (override via `RequestOptions.baseUrlOverride`)\n- Token: `FcmsConfig.tokenProvider` (override via `RequestOptions.tokenOverride`)\n- Timeouts: connect/read/write timeouts globally; per-call read timeout via `RequestOptions.readTimeoutMillisOverride`\n- Dispatcher concurrency: `maxRequests`, `maxRequestsPerHost`\n- Retries: opt-in via `enableRetries`; only idempotent GETs retry by default; 429 Retry-After respected\n\n## Error Handling\n\nNon-2xx responses throw `ly.neptune.nexus.fcms.core.http.FcmsHttpException` with:\n\n- `status` (HTTP)\n- `code` and `message` (if available from body)\n- `body` (raw)\n- `headers` (map) and `retryAfterSeconds` (parsed if present)\n\n## Pagination\n\nList APIs return `Page\u003cT\u003e` with fields: `data`, `total`, `perPage`, `currentPage`, `next`, `prev`. Examples:\n\n- Salaries: `listTransactions(page)` → `Page\u003cTransaction\u003e`\n- Accounts: `listAccounts(page, filter)` → `Page\u003cBankAccount\u003e`\n- Requests: `listPendingRequests(page)` → `Page\u003cPurchaseRequestQueueItem\u003e`\n\n- Laravel root links object `{ links: { next, prev } }` supported\n- Laravel `meta.links` array supported\n\n## Query parameters and filtering\n\nThe SDK builds URLs and query strings for you. Here’s how to pass filters safely:\n\n- Salaries `GET {baseUrl}/api/v1/mof/transactions`\n  - Use `SalariesListFilter` with `listTransactionsFiltered(...)` to set Laravel-style filters:\n\nKotlin:\n```kotlin\nval salariesPage = salaries.listTransactionsFiltered(\n    page = 1,\n    filter = SalariesListFilter(\n        state = \"pending\",   // -\u003e filter[state]=pending\n        year = 2025,          // -\u003e filter[year]=2025\n        month = 8             // -\u003e filter[month]=8\n    )\n)\n\n// Convenience helpers\nval byState = salaries.listTransactionsByState(\"pending\", page = 1)\nval byYear = salaries.listTransactionsByYear(2025, page = 1)\nval byYearMonth = salaries.listTransactionsByYearMonth(2025, 8, page = 1)\nval byAll = salaries.listTransactionsByYearMonthState(2025, 8, \"completed\", page = 1)\n\n// Raw map (advanced)\nval raw = salaries.listTransactionsWithFilters(\n    page = 1,\n    filters = mapOf(\n        \"filter[state]\" to \"pending\",\n        \"filter[year]\" to \"2025\"\n    )\n)\n```\n\nJava:\n```java\n// Typed filter\nPage\u003cTransaction\u003e page = client\n  .listTransactions(1, new SalariesListFilter(\"pending\", 2025, 8), null)\n  .get();\n\n// Convenience wrappers\nPage\u003cTransaction\u003e byState = client.listTransactionsByState(\"pending\", 1, null).get();\nPage\u003cTransaction\u003e byYear = client.listTransactionsByYear(2025, 1, null).get();\nPage\u003cTransaction\u003e byYearMonth = client.listTransactionsByYearMonth(2025, 8, 1, null).get();\nPage\u003cTransaction\u003e byAll = client.listTransactionsByYearMonthState(2025, 8, \"completed\", 1, null).get();\n\n// Raw map\nPage\u003cTransaction\u003e raw = client\n  .listTransactionsWithFilters(1, Map.of(\"filter[state]\", \"pending\", \"filter[year]\", \"2025\"), null)\n  .get();\n```\n\n- Accounts `GET {baseUrl}/api/v1/bank-accounts`\n  - Use `AccountsListFilter` to set query params that map to Laravel-style filter keys:\n\nKotlin:\n```kotlin\nval accountsPage = accounts.listAccounts(\n    page = 1,\n    filter = AccountsListFilter(\n        state = \"pending\",               // -\u003e filter[state]=pending\n        iban = \"SA123...\",               // -\u003e filter[iban]=SA123...\n        createdOn = \"2025-01-15\",        // -\u003e filter[created_on]=2025-01-15\n        approvedOn = null,                // omitted when null/blank\n        rejectedOn = null,\n        unrejectedOn = null,\n        accountNumber = null,             // -\u003e filter[account_number]=...\n        hasAccountNumber = true           // -\u003e filter[has_account_number]=true\n    )\n)\n```\n\nJava:\n```java\nAccountsListFilter filter = new AccountsListFilter(\n    \"pending\",      // state -\u003e filter[state]=pending\n    null,            // iban\n    null,            // createdOn (yyyy-MM-dd)\n    null,            // approvedOn\n    null,            // rejectedOn\n    null,            // unrejectedOn\n    null,            // accountNumber\n    Boolean.TRUE     // hasAccountNumber -\u003e filter[has_account_number]=true\n);\nPage\u003cBankAccount\u003e page = accountsClient.listAccounts(1, filter, null);\n```\n\nNotes:\n- Only one question mark is used in a URL. Example with multiple filters: `...?filter[state]=pending\u0026filter[year]=2025` (not `\u0026?filter[year]=...`).\n- Null/blank fields are omitted from the query string automatically.\n\n## Salaries Data Models\n\nThe SDK maps Salaries API JSON into typed models. New fields added in 1.0.3 are marked; nullability updates in 1.0.4 are noted.\n\n- Transaction\n  - `uuid: String`\n  - `state: String`\n  - `individual: Individual`\n  - `bankAccount: BankAccount`\n  - `salary: Salary`\n  - `entity: Entity?` (new in 1.0.3)\n  - `description: String?` (new in 1.0.3)\n\n- Individual\n  - `name: String`, `nid: String`, `mofFinancialNumber: String`, `phoneNumber: String?`\n\n- BankAccount\n  - `number: String`, `iban: String?`, `bankBranch: String?` (new in 1.0.3)\n\n- Salary\n  - `amount: BigDecimal` (string or numeric in JSON supported)\n  - `currency: String`\n  - `period: Period { year: String, month: String }`\n\n- Entity (new in 1.0.3)\n  - `name: String?` (nullable since 1.0.4), `region: String?`\n\nExample (truncated):\n```json\n{\n  \"uuid\": \"...\",\n  \"state\": \"pending\",\n  \"individual\": { \"name\": \"...\", \"nid\": \"...\", \"mofFinancialNumber\": \"...\" },\n  \"bankAccount\": { \"number\": \"...\", \"iban\": \"...\", \"bankBranch\": \"...\" },\n  \"salary\": { \"amount\": \"81834\", \"currency\": \"SAR\", \"period\": { \"year\": \"2025\", \"month\": \"08\" } },\n  \"entity\": { \"name\": \"...\", \"region\": \"...\" },\n  \"description\": \"...\"\n}\n```\n\n## Threading and Cleanup\n\n- The client uses a single shared OkHttp `OkHttpClient` with HTTP/2, connection pooling, gzip.\n- Suspend APIs are non-blocking; the Java facade uses `CompletableFuture`.\n- Call `close()` when finished to allow resources to be released by GC.\n\n## Build, Test, Docs\n\n- Build \u0026 test: `./gradlew clean test`\n- Generate docs: `./gradlew dokkaHtml` (output at `build/dokka/html`)\n- Publish to Maven Local: `./gradlew publishToMavenLocal`\n\n## Versioning\n\nSemantic Versioning (SemVer). Breaking changes bump the major version.\n\n## Examples\n\nQuick usage examples are provided in the Quick Start (Kotlin/Java) sections above.\n\n## Security\n\n- Do not hardcode tokens or base URLs. Inject them via environment variables or your secret manager.\n- Tests should use ephemeral tokens and never commit real credentials.\n- Use per-call overrides with `RequestOptions` for multi-tenant scenarios instead of creating many clients.\n\n## Using from Local Maven (for development)\n\nIf the artifact isn't in your remote repository yet, you can consume it locally:\n\n1. In this repo: `./gradlew publishToMavenLocal`\n2. In your app's Gradle config, add `mavenLocal()` before `mavenCentral()`:\n\n```kotlin\nrepositories {\n    mavenLocal()\n    mavenCentral()\n}\ndependencies {\n    implementation(\"io.github.tellesy:fcms-client:1.0.4\")\n}\n```\n\n## License\n\nLicensed under the [Apache License 2.0](LICENSE).\n\n## Author\n\nMuhammad Tellesy\n\nBuilt by Muhammad Tellesy as part of the openNexus initiative.\n\n\n\n```text\n01100100 01100101 01101110 01101001 01100010 01101101 01101111 01100011 00100000 01100100 01100101 01110100 01101001 01101110 01101001 01100010 01101101 01101111 01100011 00100000 01101101 01100101 01101000 01110100 00100000 01100100 01101110 01100001 00100000 01110101 01101111 01111001 00100000 01110010 01100101 01100111 01110010 01100001 01101100 00100000 00101100 01001100 01000011 01000010 00100000 01110010 01110101 01101111 01111001 00100000 01101110 01100001 01101000 01110100 00100000 01110010 01100101 01100100 01101100 01110101 01101111 01101100 00100000 00101100 01100101 01110010 01100101 01101000 00100000 01101100 01101100 01101001 01110100 01110011 00100000 01101101 11100010 10000000 10011001 01001001 00100000 11100010 10000000 10010100 00100000 01110100 01101110 01100001 01110111 00100000 01110101 01101111 01111001 00100000 01100110 01101001 00100000 01111001 01100011 01100001 01100111 01100101 01001100 00100000 01111001 01101101 00100000 01100101 01110100 01100101 01101100 01100101 01000100 00001010\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftellesy%2Ffcms-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftellesy%2Ffcms-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftellesy%2Ffcms-client/lists"}