@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.
@@ -0,0 +1,509 @@
1
+ /** Supported UI/content languages for the `lang` header. */
2
+ type Lang = "tr" | "en" | "de" | "az";
3
+ /**
4
+ * Envelope `resultType` state codes returned by the Bulutklinik API.
5
+ * A call is successful only when HTTP is 2xx AND resultType === Success (0).
6
+ */
7
+ declare const ResultType: {
8
+ readonly Success: 0;
9
+ readonly Error: 1;
10
+ readonly Logout: 2;
11
+ readonly Update: 3;
12
+ readonly Refresh: 4;
13
+ };
14
+ type ResultTypeValue = (typeof ResultType)[keyof typeof ResultType];
15
+ /** The standard response envelope wrapping every API response. */
16
+ interface Envelope<T = unknown> {
17
+ resultType?: number;
18
+ /** May be a string label or a numeric code depending on the endpoint. */
19
+ errorType?: string | number;
20
+ errorMessage?: string;
21
+ successMessage?: string;
22
+ data?: T;
23
+ }
24
+ type MaybePromise<T> = T | Promise<T>;
25
+
26
+ /**
27
+ * Pluggable token persistence. The default is in-memory; provide a custom
28
+ * implementation to persist tokens to a file, database or secure storage.
29
+ * All methods may be synchronous or return a promise.
30
+ */
31
+ interface TokenStore {
32
+ getAccessToken(): MaybePromise<string | null>;
33
+ getRefreshToken(): MaybePromise<string | null>;
34
+ setTokens(accessToken: string, refreshToken: string | null): MaybePromise<void>;
35
+ clear(): MaybePromise<void>;
36
+ }
37
+ /** In-memory token store (default). Tokens are lost when the process exits. */
38
+ declare class MemoryTokenStore implements TokenStore {
39
+ #private;
40
+ constructor(seed?: {
41
+ accessToken?: string | null;
42
+ refreshToken?: string | null;
43
+ });
44
+ getAccessToken(): string | null;
45
+ getRefreshToken(): string | null;
46
+ setTokens(accessToken: string, refreshToken: string | null): void;
47
+ clear(): void;
48
+ }
49
+
50
+ type Environment = "production" | "test" | "local";
51
+ declare const ENVIRONMENT_BASE_URLS: Record<Environment, string>;
52
+ type FetchLike = typeof fetch;
53
+ interface ClientOptions {
54
+ /** Named environment preset. Ignored when `baseUrl` is provided. Default: `production`. */
55
+ environment?: Environment;
56
+ /** Explicit base URL (e.g. `https://api.bulutklinik.com/api/v3`). Overrides `environment`. */
57
+ baseUrl?: string;
58
+ /** Default `lang` header. Default: `tr`. */
59
+ lang?: Lang;
60
+ /** OAuth client id — used by `auth.connect` and for token refresh. */
61
+ clientId?: string;
62
+ /** OAuth client secret — used by `auth.connect` and for token refresh. */
63
+ clientSecret?: string;
64
+ /** Bearer token for the partner (`teusan`) endpoint. */
65
+ partnerToken?: string;
66
+ /** Pluggable token persistence. Default: in-memory. */
67
+ tokenStore?: TokenStore;
68
+ /** Per-request timeout in milliseconds. Default: 30000. */
69
+ timeoutMs?: number;
70
+ /** Injectable fetch implementation. Default: global `fetch`. */
71
+ fetch?: FetchLike;
72
+ }
73
+ interface ResolvedConfig {
74
+ baseUrl: string;
75
+ lang: Lang;
76
+ clientId?: string;
77
+ clientSecret?: string;
78
+ partnerToken?: string;
79
+ tokenStore: TokenStore;
80
+ timeoutMs: number;
81
+ fetchImpl: FetchLike;
82
+ }
83
+
84
+ type AuthMode = "public" | "bearer" | "partner";
85
+ interface RequestSpec {
86
+ method: "GET" | "POST" | "PUT" | "DELETE";
87
+ path: string;
88
+ auth: AuthMode;
89
+ body?: unknown;
90
+ lang?: Lang;
91
+ }
92
+ /**
93
+ * Low-level transport. Builds requests, unwraps the response envelope, maps
94
+ * failures to typed errors, and performs a single silent token refresh + retry
95
+ * on a 401 / `resultType 4`. Concurrent refreshes share one in-flight promise.
96
+ */
97
+ declare class HttpClient {
98
+ readonly tokenStore: TokenStore;
99
+ readonly clientId: string | undefined;
100
+ readonly clientSecret: string | undefined;
101
+ private readonly baseUrl;
102
+ private readonly lang;
103
+ private readonly partnerToken;
104
+ private readonly timeoutMs;
105
+ private readonly fetchImpl;
106
+ private refreshInFlight;
107
+ constructor(config: ResolvedConfig);
108
+ request<T>(spec: RequestSpec): Promise<T>;
109
+ /** Force a token refresh using the stored refresh token. Throws on failure. */
110
+ refresh(): Promise<void>;
111
+ private send;
112
+ private dispatch;
113
+ private readEnvelope;
114
+ private ensureRefreshed;
115
+ private performRefresh;
116
+ private toError;
117
+ }
118
+
119
+ type LoginMode = "email" | "identity" | "phone" | "user_id" | "social" | "afterRegister";
120
+ interface ConnectInput {
121
+ apiUserName: string;
122
+ /** Required except `social` / `afterRegister` modes. */
123
+ apiUserPassword?: string;
124
+ loginMode: LoginMode;
125
+ /** Defaults to the client's configured `clientId`. */
126
+ clientId?: string;
127
+ /** Defaults to the client's configured `clientSecret`. */
128
+ clientSecret?: string;
129
+ /** Some installs require this in `phone` mode. */
130
+ withPhoneNumber?: string;
131
+ }
132
+ interface LoginData {
133
+ access_token?: string;
134
+ refresh_token?: string;
135
+ password_policy?: unknown;
136
+ /** Present (instead of tokens) when 2FA is required — an encrypted blob. */
137
+ response?: string;
138
+ }
139
+ type LoginResult = {
140
+ twoFactorRequired: false;
141
+ passwordPolicy?: unknown;
142
+ } | {
143
+ twoFactorRequired: true;
144
+ twoFactorResponse: string;
145
+ };
146
+ interface TwoFactorInput {
147
+ smsVerificationCode: string;
148
+ /** The encrypted blob from `connect`'s 2FA challenge (`twoFactorResponse`). */
149
+ response: string;
150
+ }
151
+ interface RegisterInput {
152
+ name: string;
153
+ surname: string;
154
+ /** Should equal `phoneNumber` (the `+CC` form) — used as the afterRegister username. */
155
+ apiUserName: string;
156
+ /** Must start with `+` and country code, e.g. `+90 555 111 22 33`. */
157
+ phoneNumber: string;
158
+ password: string;
159
+ smsVerificationCode: string;
160
+ /** Encrypted blob from the prior SMS-verification step. */
161
+ response: string;
162
+ acceptUserAgreement?: 0 | 1;
163
+ clientId?: string;
164
+ clientSecret?: string;
165
+ }
166
+ type DoctorListType = "interview" | "appointment";
167
+ interface QuickSearchInput {
168
+ searchText: string;
169
+ listType?: DoctorListType | null;
170
+ location?: string | null;
171
+ }
172
+ interface QuickSearchResult {
173
+ searchedBranches?: unknown[];
174
+ searchedDoctors?: unknown[];
175
+ searchedCompanies?: unknown[];
176
+ searchedGivenTreatments?: unknown[];
177
+ searchedBlogs?: unknown[];
178
+ queryText?: string;
179
+ [k: string]: unknown;
180
+ }
181
+ interface SearchParams {
182
+ withFreeText?: string;
183
+ withDoctorName?: string;
184
+ withBranchName?: string;
185
+ /** `-1` excludes psychology/diet. */
186
+ withBranchId?: number | null;
187
+ withLocationName?: string;
188
+ withLocationId?: number | null;
189
+ withCompanyName?: string;
190
+ withCompanyId?: number | null;
191
+ withGivenTreatments?: string;
192
+ withExpertyId?: number | null;
193
+ withInstitutionId?: number | null;
194
+ withNearestSlotDayRange?: number | null;
195
+ }
196
+ type OrderParam = "name" | "point" | "slot" | "order";
197
+ type OtherParam = "isKizilay" | "isQuestionable" | "isInterviewable" | "isAppointmentable";
198
+ interface DoctorSearchInput {
199
+ searchParams?: SearchParams;
200
+ orderParams?: OrderParam[];
201
+ otherParams?: OtherParam[];
202
+ /** >= 1. */
203
+ currentPage: number;
204
+ /** 10–100. Default 20. */
205
+ perPageLimit?: number;
206
+ }
207
+ interface DoctorSummary {
208
+ doctor_id: number;
209
+ name?: string;
210
+ surname?: string;
211
+ branch_name?: string;
212
+ star_rate?: number;
213
+ nearest_slot?: string | null;
214
+ isInterviewable?: boolean;
215
+ isAppointmentable?: boolean;
216
+ url?: string;
217
+ user_image?: string;
218
+ [k: string]: unknown;
219
+ }
220
+ interface DoctorSearchResult {
221
+ foundDoctorsCount: number;
222
+ foundDoctors: DoctorSummary[];
223
+ [k: string]: unknown;
224
+ }
225
+ interface Branch {
226
+ sysbrnch_id?: number;
227
+ branch_name?: string;
228
+ [k: string]: unknown;
229
+ }
230
+ interface Location {
231
+ location_id?: number;
232
+ [k: string]: unknown;
233
+ }
234
+ type DoctorDetail = Record<string, unknown>;
235
+ interface SchedulerInput {
236
+ doctorId: number | string;
237
+ /** `Y-m-d`, today..+21. When omitted, `scheduleStep` + `schedulePage` page the window. */
238
+ scheduleDate?: string | null;
239
+ scheduleStep?: number | string;
240
+ schedulePage?: number | string;
241
+ /** `interview` → online slots; anything else → physical. */
242
+ listType: DoctorListType;
243
+ }
244
+ interface Slot {
245
+ slotId: number;
246
+ /** `HH:mm:ss`. */
247
+ slotStart: string;
248
+ /** `HH:mm:ss`. */
249
+ slotEnd: string;
250
+ available: boolean;
251
+ }
252
+ /** Date-keyed (`Y-m-d`) map → slots for that day. Empty days are `[]`. */
253
+ type SchedulerResult = Record<string, Slot[]>;
254
+ type AppointmentType = "interview" | "appointment";
255
+ interface ReserveInterviewInput {
256
+ doctorId: number | string;
257
+ /** `Y-m-d H:i`, today..+21. */
258
+ appointmentDate: string;
259
+ appointmentType?: AppointmentType;
260
+ }
261
+ interface PhysicalAppointmentInput {
262
+ doctorId: number | string;
263
+ /** `Y-m-d H:i`. */
264
+ appointmentDate: string;
265
+ }
266
+ type DiscountCheckType = "question" | "appointment" | "lab" | "special" | "physicallyAppointment" | "tmcLab" | "program";
267
+ interface DiscountCheckInput {
268
+ checkType: DiscountCheckType;
269
+ discountCode: string;
270
+ /** Required except `lab` / `tmcLab` / `program`. */
271
+ doctorId?: number | string;
272
+ orderId?: number | string;
273
+ specialServiceId?: number | string;
274
+ programSlug?: string;
275
+ }
276
+ type DiscountResult = Record<string, unknown>;
277
+ interface CardInfo {
278
+ cardHolder: string;
279
+ cardNumber: string;
280
+ /** `m`. */
281
+ cardExpMonth: string;
282
+ /** `Y`. */
283
+ cardExpYear: string;
284
+ cardCvv: string;
285
+ }
286
+ interface SavedCard {
287
+ id: number;
288
+ card_holder_name?: string;
289
+ /** Masked. */
290
+ card_number?: string;
291
+ card_type?: string;
292
+ created_at?: string;
293
+ [k: string]: unknown;
294
+ }
295
+ interface PaymentInput {
296
+ doctorId: number | string;
297
+ /** `Y-m-d H:i`. */
298
+ appointmentDate: string;
299
+ appointmentType?: AppointmentType;
300
+ is3D: boolean;
301
+ termsAccept: boolean;
302
+ /** A new card (all-or-none) … */
303
+ cardInfo?: CardInfo;
304
+ /** … or a saved card id (from `getCards`). */
305
+ cardId?: number | string;
306
+ /** `1` tokenizes the card. */
307
+ saveCard?: 0 | 1;
308
+ discountCode?: string;
309
+ /** Opaque encrypted blob, passed through verbatim. */
310
+ caseDetail?: string;
311
+ }
312
+ /** On 3DS success, `payment3DUrl` is a browser URL to open. */
313
+ interface PaymentResult {
314
+ payment3DUrl?: string;
315
+ [k: string]: unknown;
316
+ }
317
+ type MeasureType = "tension" | "glucose" | "pulse" | "fever" | "weight" | "length" | "waist" | "hip" | "fat" | "muscle" | "calorie" | "step" | "sleep";
318
+ /** 1=day, 2=week, 3=month, 4=year. */
319
+ type GraphPeriod = 1 | 2 | 3 | 4;
320
+ /** A single measurement. `date_time` is `Y-m-d H:i`; other fields depend on `type`. */
321
+ interface MeasureRecord {
322
+ type: MeasureType;
323
+ date_time: string;
324
+ [field: string]: string | number;
325
+ }
326
+ /** Fields for the single-type endpoints (`date_time` + the type's own fields). */
327
+ interface MeasureFields {
328
+ date_time: string;
329
+ [field: string]: string | number;
330
+ }
331
+ interface PartnerHealthInput {
332
+ identity?: string;
333
+ phoneNumber?: string;
334
+ data: MeasureRecord[];
335
+ }
336
+
337
+ /** Online reservation, physical appointment and cancellation. */
338
+ declare class AppointmentsResource {
339
+ private readonly http;
340
+ constructor(http: HttpClient);
341
+ /** Reserve an online (interview) slot. Resolves to `null` on success. */
342
+ reserveInterview(input: ReserveInterviewInput): Promise<null>;
343
+ /** Create a physical appointment. */
344
+ addPhysical(input: PhysicalAppointmentInput): Promise<unknown>;
345
+ /** Cancel an appointment by event id (`cln_events.id`). */
346
+ cancel(eventId: number | string): Promise<unknown>;
347
+ }
348
+
349
+ /** Login, 2FA, token refresh, registration and logout. */
350
+ declare class AuthResource {
351
+ private readonly http;
352
+ constructor(http: HttpClient);
353
+ /**
354
+ * Log in. On success tokens are stored automatically and
355
+ * `{ twoFactorRequired: false }` is returned. If 2FA is enabled the result is
356
+ * `{ twoFactorRequired: true, twoFactorResponse }` — pass that blob to
357
+ * {@link connectWithTwoFactor} together with the SMS code.
358
+ */
359
+ connect(input: ConnectInput): Promise<LoginResult>;
360
+ /** Complete a 2FA login with the SMS code and the challenge blob. */
361
+ connectWithTwoFactor(input: TwoFactorInput): Promise<void>;
362
+ /** Register a new patient (afterRegister auto-login). Stores tokens on success. */
363
+ register(input: RegisterInput): Promise<void>;
364
+ /** Manually refresh the access token using the stored refresh token. */
365
+ refresh(): Promise<void>;
366
+ /** Revoke the current tokens server-side and clear the local token store. */
367
+ disconnect(): Promise<void>;
368
+ private storeTokens;
369
+ }
370
+
371
+ /** Branches, locations, quick/filtered doctor search and doctor detail. */
372
+ declare class DoctorsResource {
373
+ private readonly http;
374
+ constructor(http: HttpClient);
375
+ branches(): Promise<Branch[]>;
376
+ locations(): Promise<Location[]>;
377
+ quickSearch(input: QuickSearchInput): Promise<QuickSearchResult>;
378
+ search(input: DoctorSearchInput): Promise<DoctorSearchResult>;
379
+ detail(id: number | string, corporate?: number | string): Promise<DoctorDetail>;
380
+ }
381
+
382
+ /** Health measurements: CRUD, latest, history list, graph and partner submission. */
383
+ declare class MeasuresResource {
384
+ private readonly http;
385
+ constructor(http: HttpClient);
386
+ /** Submit multiple measurements of any types in one call (primary entrypoint). */
387
+ addList(records: MeasureRecord[]): Promise<unknown>;
388
+ /** Submit a single measurement of one type. */
389
+ add(type: MeasureType, fields: MeasureFields): Promise<unknown>;
390
+ update(type: MeasureType, input: {
391
+ id: number | string;
392
+ } & MeasureFields): Promise<unknown>;
393
+ delete(type: MeasureType, id: number | string): Promise<unknown>;
394
+ /** Latest value of each measurement type. */
395
+ last(): Promise<Record<string, unknown>>;
396
+ /** Paginated history for one type. `glucoseType` (0/1) applies only to glucose. */
397
+ list(type: MeasureType, page: number | string, glucoseType?: 0 | 1): Promise<unknown>;
398
+ /** Grouped graph data. `period`: 1=day, 2=week, 3=month, 4=year. */
399
+ graph(type: MeasureType, period: GraphPeriod, page: number | string, glucoseType?: 0 | 1): Promise<unknown>;
400
+ /** Partner (teusan) submission — uses the configured partner token. */
401
+ partnerHealthInformation(input: PartnerHealthInput): Promise<unknown>;
402
+ }
403
+
404
+ /** Discount check, saved cards and the 3DS payment entrypoint. */
405
+ declare class PaymentsResource {
406
+ private readonly http;
407
+ constructor(http: HttpClient);
408
+ /** Validate a discount code. Note: this endpoint lives under the `patients` prefix. */
409
+ checkDiscountCode(input: DiscountCheckInput): Promise<DiscountResult>;
410
+ getCards(): Promise<{
411
+ cards: SavedCard[];
412
+ }>;
413
+ saveCard(card: CardInfo): Promise<unknown>;
414
+ /**
415
+ * Start an appointment payment. The amount is computed server-side. On a 3DS
416
+ * flow the result carries `payment3DUrl` — a browser URL to open; the SDK does
417
+ * not follow it. 3DS capture happens via the bank → server callback.
418
+ */
419
+ pay(input: PaymentInput): Promise<PaymentResult>;
420
+ deleteCard(cardId: number | string): Promise<unknown>;
421
+ }
422
+
423
+ /** Doctor availability (materialized slots). */
424
+ declare class SlotsResource {
425
+ private readonly http;
426
+ constructor(http: HttpClient);
427
+ /**
428
+ * Fetch a doctor's free slots. Returns a date-keyed map of slots. Build the
429
+ * next step's `appointmentDate` as `"<date> <slotStart>"` (drop the seconds).
430
+ */
431
+ schedule(input: SchedulerInput): Promise<SchedulerResult>;
432
+ }
433
+
434
+ /**
435
+ * The Bulutklinik API client. Construct once and reuse; service groups are
436
+ * exposed as properties.
437
+ *
438
+ * @example
439
+ * ```ts
440
+ * const client = new BulutklinikClient({
441
+ * environment: "test",
442
+ * clientId: "…",
443
+ * clientSecret: "…",
444
+ * });
445
+ * await client.auth.connect({ apiUserName: "…", apiUserPassword: "…", loginMode: "email" });
446
+ * const result = await client.doctors.quickSearch({ searchText: "kardiyo" });
447
+ * ```
448
+ */
449
+ declare class BulutklinikClient {
450
+ readonly auth: AuthResource;
451
+ readonly doctors: DoctorsResource;
452
+ readonly slots: SlotsResource;
453
+ readonly appointments: AppointmentsResource;
454
+ readonly payments: PaymentsResource;
455
+ readonly measures: MeasuresResource;
456
+ /** The active token store (also accepts a custom one via options). */
457
+ readonly tokenStore: TokenStore;
458
+ private readonly http;
459
+ constructor(options?: ClientOptions);
460
+ }
461
+
462
+ interface ApiErrorContext {
463
+ httpStatus: number;
464
+ resultType?: number;
465
+ errorType?: string | number;
466
+ data?: unknown;
467
+ method: string;
468
+ path: string;
469
+ retryAfter?: number;
470
+ }
471
+ /** Base class for every error thrown by the SDK. */
472
+ declare class BulutklinikError extends Error {
473
+ constructor(message: string, options?: {
474
+ cause?: unknown;
475
+ });
476
+ }
477
+ /** Network failure, timeout, DNS or TLS error — no HTTP response was received. */
478
+ declare class TransportError extends BulutklinikError {
479
+ constructor(message: string, cause?: unknown);
480
+ }
481
+ /** An HTTP response was received but the call was not successful. */
482
+ declare class ApiError extends BulutklinikError {
483
+ readonly httpStatus: number;
484
+ readonly resultType: number | undefined;
485
+ readonly errorType: string | number | undefined;
486
+ readonly data: unknown;
487
+ readonly method: string;
488
+ readonly path: string;
489
+ constructor(message: string, ctx: ApiErrorContext);
490
+ }
491
+ /** 422 / errorType=validation. */
492
+ declare class ValidationError extends ApiError {
493
+ }
494
+ /** 401, a logout (resultType 2), or a failed token refresh. */
495
+ declare class AuthenticationError extends ApiError {
496
+ }
497
+ /** 403 — authenticated but not permitted / out of scope. */
498
+ declare class AuthorizationError extends ApiError {
499
+ }
500
+ /** 404. */
501
+ declare class NotFoundError extends ApiError {
502
+ }
503
+ /** 429 — throttled. Carries `retryAfter` (seconds) when the header is present. */
504
+ declare class RateLimitError extends ApiError {
505
+ readonly retryAfter: number | undefined;
506
+ constructor(message: string, ctx: ApiErrorContext);
507
+ }
508
+
509
+ export { ApiError, type AppointmentType, AppointmentsResource, AuthResource, AuthenticationError, AuthorizationError, type Branch, BulutklinikClient, BulutklinikError, type CardInfo, type ClientOptions, type ConnectInput, type DiscountCheckInput, type DiscountCheckType, type DiscountResult, type DoctorDetail, type DoctorListType, type DoctorSearchInput, type DoctorSearchResult, type DoctorSummary, DoctorsResource, ENVIRONMENT_BASE_URLS, type Envelope, type Environment, type FetchLike, type GraphPeriod, type Lang, type Location, type LoginData, type LoginMode, type LoginResult, type MeasureFields, type MeasureRecord, type MeasureType, MeasuresResource, MemoryTokenStore, NotFoundError, type OrderParam, type OtherParam, type PartnerHealthInput, type PaymentInput, type PaymentResult, PaymentsResource, type PhysicalAppointmentInput, type QuickSearchInput, type QuickSearchResult, RateLimitError, type RegisterInput, type ReserveInterviewInput, ResultType, type ResultTypeValue, type SavedCard, type SchedulerInput, type SchedulerResult, type SearchParams, type Slot, SlotsResource, type TokenStore, TransportError, type TwoFactorInput, ValidationError };