@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 +482 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/index.cjs +647 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +509 -0
- package/dist/index.d.ts +509 -0
- package/dist/index.js +628 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
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
|