@bulutklinik/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DESIGN.md ADDED
@@ -0,0 +1,482 @@
1
+ # Bulutklinik SDK — Canonical Design (SSOT)
2
+
3
+ > **This file is the single source of truth (SSOT) for every official Bulutklinik
4
+ > SDK.** All language packages (JavaScript/TypeScript, PHP, Python, Go, Java, C#,
5
+ > C++) are hand-written but MUST implement exactly the contract described here.
6
+ > The canonical copy lives at `dev-kits/DESIGN.md`; an identical copy is vendored
7
+ > into each language repository and re-synced whenever this file changes.
8
+ >
9
+ > Wire contract is derived from `dev-kits/Bulutklinik.postman_collection.json`
10
+ > ("Bulutklinik API — Randevu & Ödeme Akışı"), validated against the BulutklinikAPI
11
+ > source (Laravel 8.12, OAuth2/Passport).
12
+
13
+ - **Spec version:** 0.1.0 (validated against the TypeScript reference SDK, live against the `test` environment)
14
+ - **API:** BulutklinikAPI v3
15
+ - **Scope:** 6 services / 27 endpoints (patient persona). Designed to grow.
16
+
17
+ ---
18
+
19
+ ## 1. Scope
20
+
21
+ The SDKs cover the patient appointment-and-payment flow plus health measurements:
22
+
23
+ | Service | Endpoints | Purpose |
24
+ |----------------|:---------:|------------------------------------------------------|
25
+ | `auth` | 5 | Login, 2FA, token refresh, registration, logout |
26
+ | `doctors` | 5 | Branches, locations, quick/filtered search, detail |
27
+ | `slots` | 1 | Doctor availability (materialized slots) |
28
+ | `appointments` | 3 | Online reservation, physical appointment, cancel |
29
+ | `payments` | 5 | Discount check, saved cards, pay (3DS) |
30
+ | `measures` | 8 | Health measurements (CRUD, list, graph, partner) |
31
+
32
+ Out of scope for this collection (may be added later): "Anlık randevu" (programs),
33
+ video-call (calls). The SDK surface is designed so new services slot in as new
34
+ resource groups without breaking existing ones.
35
+
36
+ ---
37
+
38
+ ## 2. Environments & transport
39
+
40
+ ### 2.1 Base URLs
41
+
42
+ | Env | Base URL |
43
+ |--------------|------------------------------------------------|
44
+ | `production` | `https://api.bulutklinik.com/api/v3` |
45
+ | `test` | `https://apitest.bulutklinik.com/api/v3` |
46
+ | `local` | `https://api-bulutklinik.test/api/v3` (Herd) |
47
+
48
+ The client accepts either a named environment preset or an explicit base URL.
49
+ Default: `production`.
50
+
51
+ ### 2.2 Required headers
52
+
53
+ | Header | Value | Notes |
54
+ |----------------|--------------------------------|---------------------------------------------|
55
+ | `Accept` | `application/json` | Always. |
56
+ | `Content-Type` | `application/json` | On requests with a body. |
57
+ | `lang` | `tr` (default), `en`, `de`, `az` | Configurable per-client and per-request. |
58
+ | `Authorization`| `Bearer <accessToken>` | Protected endpoints only. Omitted on public endpoints; partner endpoint uses the partner token. |
59
+
60
+ ### 2.3 HTTP methods
61
+
62
+ Endpoints use `GET`, `POST`, `PUT`, `DELETE` as specified per endpoint in §6.
63
+ Path parameters (e.g. `{id}`, `{type}`, `{page}`) are URL segments, not query
64
+ string. Request bodies are JSON.
65
+
66
+ ---
67
+
68
+ ## 3. Response envelope
69
+
70
+ Every API response is a JSON envelope:
71
+
72
+ ```jsonc
73
+ {
74
+ "resultType": 0, // integer state code (see §3.1)
75
+ "errorType": "validation", // optional; string label OR numeric code (see note)
76
+ "errorMessage": "…", // optional, human-readable (localized via `lang`)
77
+ "successMessage": "…", // optional
78
+ "data": { /* payload */ } // endpoint-specific; may be null, object, array, or string
79
+ }
80
+ ```
81
+
82
+ > **`errorType` is polymorphic** (verified live): some endpoints return a string
83
+ > label (e.g. `"validation"`), others return a numeric code (e.g. `1`). SDKs must
84
+ > accept both — only treat `errorType` as a refinement hint when it is a string;
85
+ > never assume it is a string (e.g. don't call string methods on it unguarded).
86
+
87
+ A call is **successful** when the HTTP status is 2xx **and** `resultType == 0`.
88
+ SDKs unwrap and return `data` to the caller on success; otherwise they raise a
89
+ typed error (§4).
90
+
91
+ ### 3.1 `resultType` state machine
92
+
93
+ | Value | Name | SDK behavior |
94
+ |:-----:|----------|------------------------------------------------------------------------------|
95
+ | `0` | Success | Return `data`. |
96
+ | `1` | Error | Raise `ApiError` (or a more specific subtype based on HTTP status / `errorType`). |
97
+ | `2` | Logout | Clear the token store, raise `AuthenticationError` (session revoked). |
98
+ | `3` | Update | Raise `ApiError` with an "update required" marker (client/app too old). |
99
+ | `4` | Refresh | Token expired. **Not** returned by `refreshApi`; returned by the global handler on any protected call that receives an expired/invalid token (HTTP 401). Triggers the auto-refresh+retry flow (§5.4). |
100
+
101
+ > Implementation note: `resultType 4` is the canonical refresh signal, but a bare
102
+ > HTTP `401` (without a parseable envelope) MUST be treated identically.
103
+
104
+ ---
105
+
106
+ ## 4. Error model
107
+
108
+ All SDKs expose one error hierarchy with a common base. Names follow each
109
+ language's convention (e.g. `BulutklinikError` / `ApiError` in TS, exceptions in
110
+ PHP/Python, `error` values implementing an interface in Go, exception classes in
111
+ Java/C#/C++).
112
+
113
+ ```
114
+ BulutklinikError (base — all SDK errors derive from this)
115
+ ├── TransportError (network failure, timeout, DNS, TLS — no HTTP response)
116
+ └── ApiError (got an HTTP response that wasn't a success)
117
+ ├── ValidationError (422, or errorType=validation)
118
+ ├── AuthenticationError (401 / resultType 2 logout / failed refresh)
119
+ ├── AuthorizationError (403 — authenticated but not permitted/scoped)
120
+ ├── NotFoundError (404)
121
+ └── RateLimitError (429 — throttled; carries Retry-After if present)
122
+ ```
123
+
124
+ Each `ApiError` carries: `httpStatus`, `resultType`, `errorType`, `errorMessage`,
125
+ the raw `data`, and the originating request (method + path) for debugging.
126
+ Mapping precedence: logout (`resultType == 2`) → string `errorType == "validation"`
127
+ → HTTP status (401→Auth, 403→Authz, 404→NotFound, 422→Validation, 429→RateLimit)
128
+ → otherwise (incl. numeric `errorType`, or success HTTP with `resultType != 0`) → `ApiError`.
129
+ Because `errorType` may be numeric (§3), guard before string-matching it.
130
+
131
+ ---
132
+
133
+ ## 5. Authentication & token lifecycle
134
+
135
+ OAuth2 via Laravel Passport. Access token lifetime ~30 days, refresh token ~130
136
+ days. The token grant happens server-side inside `connectApi` (no direct
137
+ `oauth/token` HTTP call from the SDK).
138
+
139
+ ### 5.1 Login — `auth.connect`
140
+
141
+ `POST /general/connectApi` (also aliased at root `/connectApi`). **Public** (no Bearer).
142
+
143
+ Request body:
144
+
145
+ | Field | Required | Notes |
146
+ |-------------------|:--------:|------------------------------------------------------------------|
147
+ | `apiUserName` | ✓ | Identifier per `loginMode` (email / TC / phone / user_id). |
148
+ | `apiUserPassword` | ✓* | Required except `social` / `afterRegister` modes. |
149
+ | `apiClientId` | ✓ | OAuth client id. |
150
+ | `apiSecretKey` | ✓ | OAuth client secret. |
151
+ | `loginMode` | ✓ | `email` \| `identity` \| `phone` \| `user_id` \| `social` \| `afterRegister`. |
152
+ | `withPhoneNumber` | — | Some installs require it in `phone` mode. |
153
+
154
+ `loginMode` `social` / `afterRegister` skip password validation
155
+ (`validateForPassportPasswordGrant`).
156
+
157
+ Success → `data: { access_token, refresh_token, password_policy }`. The SDK
158
+ persists both tokens via the token store (§5.5).
159
+
160
+ **2FA branch:** if SMS 2FA is enabled (`sms_2fa_status=1`), `data.access_token` is
161
+ absent and `data.response` carries an encrypted blob. The SDK surfaces this as a
162
+ *two-factor challenge* (typed result, not an error) so the caller can collect the
163
+ SMS code and call `auth.connectWithTwoFactor`.
164
+
165
+ ### 5.2 2FA verification — `auth.connectWithTwoFactor`
166
+
167
+ `POST /general/connectApiWithTwoFactor`. **Public** (middleware verifies the SMS code
168
+ inside the encrypted blob).
169
+
170
+ Request body:
171
+
172
+ | Field | Required | Notes |
173
+ |------------------------|:--------:|------------------------------------------------|
174
+ | `smsVerificationCode` | ✓ | The code the user received by SMS. |
175
+ | `response` | ✓ | The encrypted blob from `connect`'s `data.response`. |
176
+
177
+ (The collection also sends `tokenInfo`, but the server ignores it — the real token
178
+ is decrypted from `response`. SDKs send only `smsVerificationCode` + `response`.)
179
+
180
+ Success → `data: { access_token, refresh_token }`. Token is **not** re-minted here;
181
+ it was minted during `connect` and is returned now.
182
+
183
+ ### 5.3 Token refresh — `auth.refresh`
184
+
185
+ `POST /general/refreshApi`. **Public.** Uses the Passport `refresh_token` grant.
186
+
187
+ Request body: `{ refreshToken, clientId, clientSecretKey }`.
188
+ Success → `data: { access_token, refresh_token }` (both rotated; persist both).
189
+
190
+ ### 5.4 Silent auto-refresh + retry (mandatory in every SDK)
191
+
192
+ On any **protected** call:
193
+
194
+ 1. Send the request with the current access token.
195
+ 2. If the response is `401` **or** `resultType == 4`, and a refresh token exists,
196
+ and this request has **not** already been retried:
197
+ a. Call `auth.refresh` with the stored refresh token + client credentials.
198
+ b. Persist the new tokens.
199
+ c. Retry the original request **once**.
200
+ 3. If the refresh call itself fails, or `resultType == 2` (logout), clear the
201
+ token store and raise `AuthenticationError`.
202
+ 4. Auto-refresh must be **concurrency-safe**: simultaneous 401s share a single
203
+ in-flight refresh (no refresh stampede). Single-threaded SDKs (e.g. plain JS)
204
+ gate on one shared promise; threaded SDKs (Java/C#/Go/C++) use a mutex.
205
+
206
+ The retry is bounded to one attempt to prevent loops.
207
+
208
+ ### 5.5 Token store (pluggable)
209
+
210
+ A `TokenStore` abstraction holds the access + refresh tokens. Default
211
+ implementation is in-memory. Consumers may inject a custom store (file, DB,
212
+ secure storage). Required operations (named per language):
213
+
214
+ - get access token / get refresh token
215
+ - set tokens (access, refresh) — atomically
216
+ - clear (on logout / revoked session)
217
+
218
+ ### 5.6 Registration — `auth.register`
219
+
220
+ `POST /patients/addNewPatient`. **Public** but guarded by SMS verification
221
+ (`checkPhoneVerificationSmsCode`) + throttle.
222
+
223
+ Request body: `name`, `surname`, `apiUserName`, `phoneNumber`, `password`,
224
+ `smsVerificationCode`, `response` (encrypted blob from the prior SMS-verify step),
225
+ `acceptUserAgreement` (1), `apiClientId`, `apiSecretKey`.
226
+
227
+ Rules (validated):
228
+ - `phoneNumber` must match `^[+]([0-9\s\(\)]*)$` — i.e. start with `+` and country
229
+ code (e.g. `+90 555 111 22 33`). Bare digits are rejected.
230
+ - `apiUserName` is used as the `afterRegister` token username; send the **same**
231
+ `+CC` value as `phoneNumber`, otherwise auto-login mints a wrong/empty token.
232
+ - Password is stored as `Hash::make(BULUT_API_ENC_KEY . password)` (bcrypt rounds=12).
233
+
234
+ Success → patient created + automatic `afterRegister` login → `data: { access_token, refresh_token }`.
235
+
236
+ > The prior SMS-verification step (`verifyAddingNewPatient`) that produces the
237
+ > `response` blob is **not in this collection**. SDKs expose `register` as-is and
238
+ > document that `response` + `smsVerificationCode` must be obtained beforehand.
239
+
240
+ ### 5.7 Logout — `auth.disconnect`
241
+
242
+ `POST /general/disconnectApi`. **Bearer required** (`auth:patients,apiusers,doctors`).
243
+ Revokes the current access + refresh tokens server-side. The SDK then clears the
244
+ token store. Optional device-token fields (firebase/ios) may be added to the body.
245
+
246
+ ---
247
+
248
+ ## 6. Endpoint reference (27)
249
+
250
+ Notation: **Canonical name** = language-neutral concept → per-language naming
251
+ follows §7. `[public]` = no auth; `[bearer]` = access token; `[partner]` = partner
252
+ token; `[scope:…]` = required OAuth scope.
253
+
254
+ ### 6.1 `auth`
255
+
256
+ | Canonical | Method | Path | Auth |
257
+ |----------------------|--------|------------------------------------|----------|
258
+ | `connect` | POST | `/general/connectApi` | public |
259
+ | `connectWithTwoFactor`| POST | `/general/connectApiWithTwoFactor` | public |
260
+ | `refresh` | POST | `/general/refreshApi` | public |
261
+ | `register` | POST | `/patients/addNewPatient` | public* |
262
+ | `disconnect` | POST | `/general/disconnectApi` | bearer |
263
+
264
+ (Bodies and responses in §5.)
265
+
266
+ ### 6.2 `doctors` `[bearer] [scope:patients,bulutweb]`
267
+
268
+ | Canonical | Method | Path | Body / params |
269
+ |----------------|--------|----------------------------------------|---------------|
270
+ | `branches` | GET | `/patients/allBranches` | — |
271
+ | `locations` | GET | `/patients/allLocations` | — |
272
+ | `quickSearch` | POST | `/patients/quickSearch` | `searchText` (3–100, req), `listType` (`interview`\|`appointment`\|null), `location` (null) |
273
+ | `search` | POST | `/patients/filteredSearch` | `searchParams{}`, `orderParams[]`, `otherParams[]`, `currentPage` (≥1, req), `perPageLimit` (10–100) |
274
+ | `detail` | GET | `/patients/doctorDetail/{id}/{corporate?}` | path `id` (req), optional `corporate` |
275
+
276
+ - `quickSearch` response: `{ searchedBranches, searchedDoctors, searchedCompanies, searchedGivenTreatments, searchedBlogs, queryText }`; each item `{ result_id, result_text, result_url, result_sub_text, result_type, result_image }`.
277
+ - `search.searchParams` keys: `withFreeText`, `withDoctorName`, `withBranchName`, `withBranchId` (`-1` excludes psychology/diet), `withLocationName`, `withLocationId`, `withCompanyName`, `withCompanyId`, `withGivenTreatments`, `withExpertyId`, `withInstitutionId`, `withNearestSlotDayRange`.
278
+ `orderParams`: `name` | `point` | `slot` | `order`. `otherParams`: `isKizilay` | `isQuestionable` | `isInterviewable` | `isAppointmentable`.
279
+ Response: `data: { foundDoctorsCount, foundDoctors: [ { doctor_id, name, surname, branch_name, star_rate, nearest_slot, isInterviewable, isAppointmentable, url, user_image, … } ] }`.
280
+ - `detail` returns `doctorGeneralInfo` (prices, session length, branch), education, languages, reviews, videos, special services, related clinics. The `doctor_id` here feeds later steps.
281
+
282
+ ### 6.3 `slots` `[bearer]`
283
+
284
+ | Canonical | Method | Path | Body |
285
+ |------------|--------|-------------------------------|------|
286
+ | `schedule` | POST | `/patients/doctorScheduler` | `doctorId` (numeric, req); `scheduleDate` (`Y-m-d`, today..+21, optional); `scheduleStep` + `schedulePage` (window paging — both required when `scheduleDate` omitted); `listType` (req: `interview` → online slot_type 1,2; else physical slot_type 0,2) |
287
+
288
+ Response: `data` = date-keyed map → for each date `[ { slotId, slotStart "HH:mm:ss", slotEnd "HH:mm:ss", available: true } ]`. Empty days are `[]`.
289
+ Next step's `appointmentDate` = `"Y-m-d H:i"` (date key + `slotStart`, **drop seconds**).
290
+
291
+ ### 6.4 `appointments` `[bearer] [scope:patients,bulutweb]`
292
+
293
+ | Canonical | Method | Path | Body / params |
294
+ |--------------------|--------|--------------------------------------------|---------------|
295
+ | `reserveInterview` | POST | `/patients/addInterviewDateReservation` | `doctorId` (numeric, req), `appointmentDate` (`Y-m-d H:i`, today..+21, req), `appointmentType` (`interview`\|`appointment`, default `interview`) |
296
+ | `addPhysical` | POST | `/patients/addNewAppointment` | `doctorId` (numeric, req), `appointmentDate` (`Y-m-d H:i`, req). No `appointmentType`. |
297
+ | `cancel` | DELETE | `/patients/deleteUserAppointment/{eventId}`| path `eventId` (= `cln_events.id`) |
298
+
299
+ `reserveInterview` success → `{ resultType: 0, data: null }`; failure → 501.
300
+ `cancel` → 501 for insurance appointments, past cancel-window, or not found.
301
+ Slot is resolved server-side from `doctorId` + `appointmentDate` (no `slotId` in request).
302
+
303
+ ### 6.5 `payments`
304
+
305
+ | Canonical | Method | Path | Auth | Notes |
306
+ |--------------------|--------|-----------------------------------|--------|-------|
307
+ | `checkDiscountCode`| POST | `/patients/checkDiscountCode` | bearer | **`patients` prefix, not `payments`.** |
308
+ | `getCards` | GET | `/payments/getCards` | bearer | |
309
+ | `saveCard` | POST | `/payments/saveCard` | bearer | Flat fields (not nested). |
310
+ | `pay` | POST | `/payments/interviewPayment` | bearer | Throttle 20/h/IP. Returns `payment3DUrl`. |
311
+ | `deleteCard` | DELETE | `/payments/deleteCard/{cardId}` | bearer | path `cardId` |
312
+
313
+ - `checkDiscountCode` body: `checkType` (`question`\|`appointment`\|`lab`\|`special`\|`physicallyAppointment`\|`tmcLab`\|`program`), `doctorId` (required except lab/tmcLab/program), `discountCode` (req), plus `orderId`/`specialServiceId`/`programSlug` per type. Valid → `data: { discount_code, discount_title, discount_id, prices }`.
314
+ - `getCards` → `data.cards[]: { id, card_holder_name, card_number (masked), card_type, created_at }`. `id` → `cardId`.
315
+ - `saveCard` body (flat, `SavePatientCardRequest`): `cardHolder`, `cardNumber`, `cardExpMonth` (`m`), `cardExpYear` (`Y`), `cardCvv` — all required.
316
+ - `pay` body: `doctorId` (req), `appointmentDate` (`Y-m-d H:i`, req), `appointmentType` (`interview`→order_type 0 / `appointment`→3), `is3D` (bool, req), `termsAccept` (accepted, req), `saveCard` (1=tokenize), `discountCode` (opt), `caseDetail` (opt, encrypted), **and** either `cardInfo{ cardHolder, cardNumber, cardExpMonth, cardExpYear, cardCvv }` (all-or-none) **or** `cardId` (saved card). Amount is computed server-side (no `amount` in request).
317
+ - `pay` response: see §8.1 (`payment3DUrl` handling).
318
+
319
+ ### 6.6 `measures`
320
+
321
+ Patient endpoints `[bearer] [scope:patients]`; partner endpoint `[partner] [scope:teusan]`.
322
+ Records are written to the authenticated patient (`bas_com_company_id` from token).
323
+
324
+ | Canonical | Method | Path | Body / params |
325
+ |----------------------------|--------|------------------------------------------------------|---------------|
326
+ | `addList` | POST | `/patients/addNewUserMeasures` | `data[]` — each item: `type` + that type's fields + `date_time`. **Primary "submit health data" endpoint.** |
327
+ | `add` | POST | `/patients/addNewUserMeasures/{type}` | path `type`; body: `date_time` + type fields |
328
+ | `update` | PUT | `/patients/updateUserMeasures/{type}` | path `type`; body: `id` (req) + fields + `date_time` |
329
+ | `delete` | DELETE | `/patients/deleteUserMeasures/{type}` | path `type`; body: `id` (req) |
330
+ | `last` | GET | `/patients/measuresList` | Latest value per type. |
331
+ | `list` | GET | `/patients/userMeasuresList/{type}/{page}/{glucoseType?}` | path; `glucoseType` 0/1 only for glucose |
332
+ | `graph` | GET | `/patients/userMeasuresGraph/{type}/{period}/{page}/{glucoseType?}` | `period` 1=day,2=week,3=month,4=year |
333
+ | `partnerHealthInformation` | POST | `/outher/healthInformation` | partner token; body: `identity`, `phoneNumber`, `data[]` |
334
+
335
+ `addList` runs in a DB transaction; submit multiple measurements in one call.
336
+ `last` returns the most-recent of each type (tension splits into hypertension/hypotension; glucose splits into `hunger_glucose`/`postprandial_glucose`), each with a `*Date`.
337
+
338
+ **Measure type schema** (every record also requires `date_time` = `"Y-m-d H:i"`):
339
+
340
+ | `type` | Fields |
341
+ |-----------|-----------------------------------------------------|
342
+ | `tension` | `hypertension` (systolic), `hypotension` (diastolic) |
343
+ | `glucose` | `glucose`, `glucose_type` (0=fasting, 1=postprandial) |
344
+ | `pulse` | `pulse` |
345
+ | `fever` | `fever` |
346
+ | `weight` | `weight` (BMI auto-computed) |
347
+ | `length` | `length` (BMI auto-computed) |
348
+ | `waist` | `waist` |
349
+ | `hip` | `hip` |
350
+ | `fat` | `fat` |
351
+ | `muscle` | `muscle` |
352
+ | `calorie` | `calorie` |
353
+ | `step` | `step` |
354
+ | `sleep` | `sleep` (hours; stored to `sleep_time`) |
355
+
356
+ Value rules: numeric; `tension`/`pulse` digits 1–10; `glucose` 0–99999.99 + `glucose_type` 0\|1; `weight`/`length` 0–99999.99; etc.
357
+
358
+ > **Known API bug (document, don't replicate):** for the partner endpoint,
359
+ > `AddNewUserMeasuresListRequest::prepareForValidation` reads `identity` from
360
+ > `$this->message` instead of `$this->identity`, nulling it during validation; in
361
+ > practice matching falls back to `phoneNumber`. The SDK sends the correct
362
+ > contract (`identity` + `phoneNumber`) and notes this in the README.
363
+
364
+ ---
365
+
366
+ ## 7. Naming conventions & API shape
367
+
368
+ The client is a single root object exposing one accessor per service group; each
369
+ group exposes the canonical methods above.
370
+
371
+ ```
372
+ client.auth.connect(...) client.payments.pay(...)
373
+ client.doctors.search(...) client.measures.addList(...)
374
+ client.slots.schedule(...) client.appointments.reserveInterview(...)
375
+ ```
376
+
377
+ Per-language casing & idioms:
378
+
379
+ | Language | Method case | Notes |
380
+ |----------|-------------|-------|
381
+ | JS/TS | `camelCase` | `client.doctors.quickSearch()`. Promise-based. |
382
+ | PHP | `camelCase` | `$client->doctors->quickSearch()`. Namespace `Bulutklinik\Sdk`. |
383
+ | Python | `snake_case`| `client.doctors.quick_search()`. Sync **and** async (`AsyncClient`). |
384
+ | Go | `PascalCase`| `client.Doctors.QuickSearch(ctx, …)`. Context-first, `(T, error)` returns. |
385
+ | Java | `camelCase` | `client.doctors().quickSearch(…)`. Builder for config; checked vs unchecked TBD in Faz 3. |
386
+ | C# | `PascalCase`+`Async` | `client.Doctors.QuickSearchAsync(…)`. `Task<T>`, `CancellationToken`. |
387
+ | C++ | `snake_case`| `client.doctors().quick_search(…)`. Namespace `bulutklinik`. cpr + nlohmann/json. |
388
+
389
+ Request inputs are typed structures (objects/records/structs) per language;
390
+ responses are typed where practical, otherwise a typed envelope + parsed `data`.
391
+
392
+ ### 7.1 Client configuration
393
+
394
+ | Option | Default | Purpose |
395
+ |---------------|----------------|----------------------------------------------------|
396
+ | `environment` / `baseUrl` | `production` | Named preset or explicit URL. |
397
+ | `lang` | `tr` | Default `lang` header; overridable per request. |
398
+ | `clientId` / `clientSecret` | — | Needed for `refresh` (and passed by `connect`). |
399
+ | `tokenStore` | in-memory | Pluggable persistence. |
400
+ | `timeout` | sane default | Request timeout. |
401
+ | `httpClient` | platform default | Injectable transport (PSR-18, http.Client, HttpClient, etc.). |
402
+
403
+ ---
404
+
405
+ ## 8. Special cases
406
+
407
+ ### 8.1 `payment3DUrl` (3-D Secure) — passthrough
408
+
409
+ `pay` success response: `{ resultType: 0, data: { payment3DUrl: "<url>" } }`.
410
+ `payment3DUrl` is a **browser URL** the SDK returns verbatim — it is one of:
411
+ (A) the bank's direct `URL_3DS`, or
412
+ (B) `{APP_URL}/api/v3/payments/threeDUrl/<token>` (our endpoint serving the 3DS HTML form).
413
+
414
+ The SDK **does not** open, follow, or parse it. 3DS completion ("provizyon
415
+ kapatma" / capture) happens browser↔bank↔server via the
416
+ `POST /api/v3/threeD/appointmentPaymentComplete/{trxId}/{driver}` callback
417
+ (`trxId = "{orderId}.{transactionUuid}.{processId}"`) — outside SDK scope.
418
+ If `is3D = false`, `data` is the inline-completed order result (no `payment3DUrl`).
419
+
420
+ ### 8.2 Encrypted blobs — passthrough
421
+
422
+ `connect`'s `data.response` (2FA), `register`'s `response`, and `caseDetail` are
423
+ opaque encrypted blobs. The SDK passes them through verbatim and never encrypts
424
+ or decrypts. The clinic/API encryption keys are never embedded in the SDK.
425
+
426
+ ### 8.3 Public vs bearer vs partner
427
+
428
+ - Public (no `Authorization`): `connect`, `connectWithTwoFactor`, `refresh`, `register`.
429
+ - Bearer (access token): everything else.
430
+ - Partner: `partnerHealthInformation` uses a separately-configured partner token
431
+ (`scope:teusan`), not the patient access token.
432
+
433
+ ---
434
+
435
+ ## 9. Cross-cutting requirements (every SDK)
436
+
437
+ 1. **Idiomatic, hand-written** — no codegen; match each ecosystem's conventions.
438
+ 2. **Minimal dependencies** — prefer the platform HTTP client; pin the documented
439
+ stack per language (see §7 / PLAN.md).
440
+ 3. **Typed** — public API and `data` payloads typed where the language supports it.
441
+ 4. **Auto-refresh + retry** per §5.4, concurrency-safe.
442
+ 5. **Pluggable** token store and HTTP client.
443
+ 6. **Errors** per §4 with full context.
444
+ 7. **Tested** — unit tests for envelope/error/refresh logic + at least one live
445
+ smoke path against `test` env (Faz 1–2).
446
+ 8. **Examples** — `examples/` with the end-to-end flow: login → search → slot →
447
+ reserve → (pay) and a measures example.
448
+ 9. **Self-contained repo** — README, LICENSE (MIT), DESIGN.md copy, CI.
449
+ 10. **Versioning** — semver; tag `vX.Y.Z` per repo.
450
+
451
+ ---
452
+
453
+ ## 10. Live validation reference (test env)
454
+
455
+ - Base: `https://apitest.bulutklinik.com/api/v3`
456
+ - OAuth client: `Patients_Web_Mobile` — id `96b630b3-f62a-4e67-b33c-b58802dca5af` (secret in the collection / env file).
457
+ - Test patient: `hackathon@bulutklinik.test` (`loginMode: email`).
458
+ - Bookable `doctorId` examples: `8282` (interview + physical), `168896` (interview).
459
+ - Known env limits (request is correct, server/env is the cause):
460
+ - `quickSearch` returns HTTP 404 / `resultType 1` on `test` — the search driver
461
+ (Elasticsearch) is unavailable there; the controller catches only
462
+ `QueryException` so other exceptions surface as a generic 404. `filteredSearch`
463
+ (`doctors.search`) works and is the production search path.
464
+ - `interviewPayment` may 404 if POS isn't configured for the company; 3DS capture
465
+ can't run from a non-browser client. SDK validation asserts the request shape +
466
+ `payment3DUrl` return, not the bank capture.
467
+ - TS reference live result (2026-06-17, `test`): 8/9 steps OK — `auth.connect`,
468
+ `doctors.branches` (136), `doctors.locations` (81), `doctors.search`,
469
+ `doctors.detail`, `slots.schedule`, `measures.last`, `auth.disconnect` all pass;
470
+ only `quickSearch` fails for the env reason above.
471
+
472
+ ---
473
+
474
+ ## 11. Change control
475
+
476
+ This file is canonical. When it changes:
477
+ 1. Bump the spec version (§ top).
478
+ 2. Copy it into every language repo (`<repo>/DESIGN.md`).
479
+ 3. Reconcile each SDK against the change; note breaking changes in repo CHANGELOGs.
480
+
481
+ If an SDK must diverge from this spec, fix the spec first (or record the
482
+ divergence here) — code and SSOT must never silently disagree.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bulutklinik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # @bulutklinik/sdk
2
+
3
+ Official Bulutklinik API SDK for JavaScript / TypeScript. Zero runtime
4
+ dependencies (uses the platform `fetch`), fully typed, ESM + CJS.
5
+
6
+ Covers the patient flow: **auth, doctor search, slots, appointments, payments,
7
+ and health measures**. See [`DESIGN.md`](./DESIGN.md) for the full wire contract.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @bulutklinik/sdk
13
+ ```
14
+
15
+ Requires Node.js >= 18 (or any runtime with a global `fetch`).
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { BulutklinikClient } from "@bulutklinik/sdk";
21
+
22
+ const client = new BulutklinikClient({
23
+ environment: "production", // "production" | "test" | "local"
24
+ clientId: process.env.BK_CLIENT_ID,
25
+ clientSecret: process.env.BK_CLIENT_SECRET,
26
+ });
27
+
28
+ // 1) Log in (tokens are stored automatically)
29
+ const login = await client.auth.connect({
30
+ apiUserName: "patient@example.com",
31
+ apiUserPassword: "•••••••",
32
+ loginMode: "email",
33
+ });
34
+
35
+ if (login.twoFactorRequired) {
36
+ // Collect the SMS code from the user, then:
37
+ await client.auth.connectWithTwoFactor({
38
+ smsVerificationCode: "123456",
39
+ response: login.twoFactorResponse,
40
+ });
41
+ }
42
+
43
+ // 2) Find a doctor
44
+ const { foundDoctors } = await client.doctors.search({
45
+ searchParams: { withFreeText: "kardiyoloji" },
46
+ orderParams: ["slot"],
47
+ otherParams: ["isInterviewable"],
48
+ currentPage: 1,
49
+ });
50
+
51
+ // 3) Get free slots
52
+ const slots = await client.slots.schedule({
53
+ doctorId: foundDoctors[0].doctor_id,
54
+ listType: "interview",
55
+ });
56
+
57
+ // 4) Reserve an online appointment ("YYYY-MM-DD HH:mm")
58
+ await client.appointments.reserveInterview({
59
+ doctorId: foundDoctors[0].doctor_id,
60
+ appointmentDate: "2026-06-20 14:30",
61
+ });
62
+ ```
63
+
64
+ ## Services
65
+
66
+ | Group | Methods |
67
+ |---------------------|---------|
68
+ | `client.auth` | `connect`, `connectWithTwoFactor`, `register`, `refresh`, `disconnect` |
69
+ | `client.doctors` | `branches`, `locations`, `quickSearch`, `search`, `detail` |
70
+ | `client.slots` | `schedule` |
71
+ | `client.appointments` | `reserveInterview`, `addPhysical`, `cancel` |
72
+ | `client.payments` | `checkDiscountCode`, `getCards`, `saveCard`, `pay`, `deleteCard` |
73
+ | `client.measures` | `addList`, `add`, `update`, `delete`, `last`, `list`, `graph`, `partnerHealthInformation` |
74
+
75
+ ## Authentication & tokens
76
+
77
+ - On `connect` / `connectWithTwoFactor` / `register`, access + refresh tokens are
78
+ stored in the token store automatically.
79
+ - On a `401` (or `resultType 4`), the SDK **silently refreshes once** and retries
80
+ the request. Concurrent calls share a single refresh.
81
+ - Provide a custom `tokenStore` to persist tokens (file, DB, secure storage):
82
+
83
+ ```ts
84
+ import type { TokenStore } from "@bulutklinik/sdk";
85
+
86
+ const tokenStore: TokenStore = {
87
+ getAccessToken: () => readFromDisk("access"),
88
+ getRefreshToken: () => readFromDisk("refresh"),
89
+ setTokens: (a, r) => writeToDisk(a, r),
90
+ clear: () => wipeDisk(),
91
+ };
92
+
93
+ const client = new BulutklinikClient({ tokenStore, clientId, clientSecret });
94
+ ```
95
+
96
+ ## Payments (3-D Secure)
97
+
98
+ `payments.pay` returns `{ payment3DUrl }` on a 3DS flow. Open that URL in a
99
+ browser; the bank → server callback completes the capture. The SDK never opens
100
+ or parses the URL.
101
+
102
+ ## Health measures
103
+
104
+ ```ts
105
+ // Submit several measurements at once (primary entrypoint)
106
+ await client.measures.addList([
107
+ { type: "tension", date_time: "2026-06-17 09:30", hypertension: 120, hypotension: 80 },
108
+ { type: "glucose", date_time: "2026-06-17 09:35", glucose: 95, glucose_type: 0 },
109
+ ]);
110
+
111
+ // Latest of each type / paginated history / graph
112
+ await client.measures.last();
113
+ await client.measures.list("glucose", 1, 0); // glucoseType 0=fasting, 1=postprandial
114
+ await client.measures.graph("tension", 2, 1); // period 2 = weekly
115
+ ```
116
+
117
+ > The partner endpoint (`partnerHealthInformation`) uses a separately-configured
118
+ > `partnerToken`. Note: the API currently matches the patient by `phoneNumber`
119
+ > (a server-side bug nulls `identity` during validation); send both for forward
120
+ > compatibility.
121
+
122
+ ## Errors
123
+
124
+ All errors extend `BulutklinikError`:
125
+
126
+ `TransportError` (network/timeout) · `ApiError` → `ValidationError` (422),
127
+ `AuthenticationError` (401 / logout), `AuthorizationError` (403),
128
+ `NotFoundError` (404), `RateLimitError` (429, `retryAfter`).
129
+
130
+ ```ts
131
+ import { ValidationError, RateLimitError } from "@bulutklinik/sdk";
132
+
133
+ try {
134
+ await client.payments.pay({ /* … */ });
135
+ } catch (err) {
136
+ if (err instanceof RateLimitError) console.log("retry after", err.retryAfter);
137
+ else if (err instanceof ValidationError) console.log("invalid:", err.data);
138
+ else throw err;
139
+ }
140
+ ```
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ npm install
146
+ npm run lint
147
+ npm run typecheck
148
+ npm test
149
+ npm run build
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT