This commit is contained in:
1301
docs/docs.go
1301
docs/docs.go
File diff suppressed because it is too large
Load Diff
1296
docs/swagger.json
1296
docs/swagger.json
File diff suppressed because it is too large
Load Diff
@@ -38,20 +38,6 @@ definitions:
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models.ErrorResponseBpjs:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
errors:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
message:
|
||||
type: string
|
||||
request_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models.MetaResponse:
|
||||
properties:
|
||||
current_page:
|
||||
@@ -106,6 +92,45 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_qris.Qris:
|
||||
properties:
|
||||
created_at:
|
||||
$ref: '#/definitions/sql.NullTime'
|
||||
display_name:
|
||||
$ref: '#/definitions/sql.NullString'
|
||||
id:
|
||||
type: string
|
||||
sort:
|
||||
$ref: '#/definitions/api-service_internal_models.NullableInt32'
|
||||
status:
|
||||
type: string
|
||||
updated_at:
|
||||
$ref: '#/definitions/sql.NullTime'
|
||||
user_created:
|
||||
$ref: '#/definitions/sql.NullString'
|
||||
user_updated:
|
||||
$ref: '#/definitions/sql.NullString'
|
||||
type: object
|
||||
api-service_internal_models_qris.QrisGetByIDResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/api-service_internal_models_qris.Qris'
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_qris.QrisGetResponse:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/api-service_internal_models_qris.Qris'
|
||||
type: array
|
||||
message:
|
||||
type: string
|
||||
meta:
|
||||
$ref: '#/definitions/api-service_internal_models.MetaResponse'
|
||||
summary:
|
||||
$ref: '#/definitions/api-service_internal_models.AggregateData'
|
||||
type: object
|
||||
api-service_internal_models_retribusi.Retribusi:
|
||||
properties:
|
||||
date_created:
|
||||
@@ -306,345 +331,6 @@ definitions:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_vclaim_peserta.PesertaData:
|
||||
properties:
|
||||
cob:
|
||||
properties:
|
||||
nmAsuransi: {}
|
||||
noAsuransi: {}
|
||||
tglTAT: {}
|
||||
tglTMT: {}
|
||||
type: object
|
||||
hakKelas:
|
||||
properties:
|
||||
keterangan:
|
||||
type: string
|
||||
kode:
|
||||
type: string
|
||||
type: object
|
||||
informasi:
|
||||
properties:
|
||||
dinsos: {}
|
||||
eSEP: {}
|
||||
noSKTM: {}
|
||||
prolanisPRB:
|
||||
type: string
|
||||
type: object
|
||||
jenisPeserta:
|
||||
properties:
|
||||
keterangan:
|
||||
type: string
|
||||
kode:
|
||||
type: string
|
||||
type: object
|
||||
mr:
|
||||
properties:
|
||||
noMR:
|
||||
type: string
|
||||
noTelepon:
|
||||
type: string
|
||||
type: object
|
||||
nama:
|
||||
type: string
|
||||
nik:
|
||||
type: string
|
||||
noKartu:
|
||||
type: string
|
||||
pisa:
|
||||
type: string
|
||||
provUmum:
|
||||
properties:
|
||||
kdProvider:
|
||||
type: string
|
||||
nmProvider:
|
||||
type: string
|
||||
type: object
|
||||
sex:
|
||||
type: string
|
||||
statusPeserta:
|
||||
properties:
|
||||
keterangan:
|
||||
type: string
|
||||
kode:
|
||||
type: string
|
||||
type: object
|
||||
tglCetakKartu:
|
||||
type: string
|
||||
tglLahir:
|
||||
type: string
|
||||
tglTAT:
|
||||
type: string
|
||||
tglTMT:
|
||||
type: string
|
||||
umur:
|
||||
properties:
|
||||
umurSaatPelayanan:
|
||||
type: string
|
||||
umurSekarang:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
api-service_internal_models_vclaim_rujukan.RujukanData:
|
||||
properties:
|
||||
diagnosa:
|
||||
properties:
|
||||
kdDiagnosa:
|
||||
type: string
|
||||
nmDiagnosa:
|
||||
type: string
|
||||
type: object
|
||||
kelasRawat:
|
||||
type: string
|
||||
nama:
|
||||
type: string
|
||||
noKartu:
|
||||
type: string
|
||||
noRujukan:
|
||||
type: string
|
||||
pelayanan:
|
||||
type: string
|
||||
poliRujukan:
|
||||
properties:
|
||||
kdPoli:
|
||||
type: string
|
||||
nmPoli:
|
||||
type: string
|
||||
type: object
|
||||
provPerujuk:
|
||||
properties:
|
||||
kdProvider:
|
||||
type: string
|
||||
nmProvider:
|
||||
type: string
|
||||
type: object
|
||||
statusRujukan:
|
||||
type: string
|
||||
tglRujukan:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_vclaim_rujukan.RujukanResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanData'
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanData'
|
||||
type: array
|
||||
message:
|
||||
type: string
|
||||
request_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_vclaim_sep.SepData:
|
||||
properties:
|
||||
catatan:
|
||||
type: string
|
||||
diagnosa:
|
||||
type: string
|
||||
informasi:
|
||||
properties:
|
||||
dpjpLayan:
|
||||
type: string
|
||||
noSKDP:
|
||||
type: string
|
||||
noTelp:
|
||||
type: string
|
||||
subSpesialis:
|
||||
type: string
|
||||
type: object
|
||||
jnsPelayanan:
|
||||
type: string
|
||||
klsRawat:
|
||||
type: string
|
||||
noMR:
|
||||
type: string
|
||||
noSep:
|
||||
type: string
|
||||
peserta:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_peserta.PesertaData'
|
||||
poli:
|
||||
type: string
|
||||
rujukan:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRujukan'
|
||||
tglSep:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_vclaim_sep.SepRequest:
|
||||
properties:
|
||||
catatan:
|
||||
type: string
|
||||
diagnosa:
|
||||
type: string
|
||||
jnsPelayanan:
|
||||
enum:
|
||||
- "1"
|
||||
- "2"
|
||||
type: string
|
||||
klsRawat:
|
||||
enum:
|
||||
- "1"
|
||||
- "2"
|
||||
- "3"
|
||||
type: string
|
||||
noKartu:
|
||||
type: string
|
||||
noMR:
|
||||
type: string
|
||||
noTelp:
|
||||
type: string
|
||||
poli:
|
||||
type: string
|
||||
ppkPelayanan:
|
||||
type: string
|
||||
request_id:
|
||||
type: string
|
||||
rujukan:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRujukan'
|
||||
tglSep:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
user:
|
||||
type: string
|
||||
required:
|
||||
- diagnosa
|
||||
- jnsPelayanan
|
||||
- klsRawat
|
||||
- noKartu
|
||||
- noMR
|
||||
- poli
|
||||
- ppkPelayanan
|
||||
- tglSep
|
||||
- user
|
||||
type: object
|
||||
api-service_internal_models_vclaim_sep.SepResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepData'
|
||||
message:
|
||||
type: string
|
||||
request_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_vclaim_sep.SepRujukan:
|
||||
properties:
|
||||
asalRujukan:
|
||||
enum:
|
||||
- "1"
|
||||
- "2"
|
||||
type: string
|
||||
noRujukan:
|
||||
type: string
|
||||
ppkRujukan:
|
||||
type: string
|
||||
tglRujukan:
|
||||
type: string
|
||||
required:
|
||||
- asalRujukan
|
||||
- noRujukan
|
||||
- ppkRujukan
|
||||
- tglRujukan
|
||||
type: object
|
||||
internal_handlers_vclaim_peserta.PesertaData:
|
||||
properties:
|
||||
cob:
|
||||
properties:
|
||||
nmAsuransi: {}
|
||||
noAsuransi: {}
|
||||
tglTAT: {}
|
||||
tglTMT: {}
|
||||
type: object
|
||||
hakKelas:
|
||||
properties:
|
||||
keterangan:
|
||||
type: string
|
||||
kode:
|
||||
type: string
|
||||
type: object
|
||||
informasi:
|
||||
properties:
|
||||
dinsos: {}
|
||||
eSEP: {}
|
||||
noSKTM: {}
|
||||
prolanisPRB:
|
||||
type: string
|
||||
type: object
|
||||
jenisPeserta:
|
||||
properties:
|
||||
keterangan:
|
||||
type: string
|
||||
kode:
|
||||
type: string
|
||||
type: object
|
||||
mr:
|
||||
properties:
|
||||
noMR:
|
||||
type: string
|
||||
noTelepon:
|
||||
type: string
|
||||
type: object
|
||||
nama:
|
||||
type: string
|
||||
nik:
|
||||
type: string
|
||||
noKartu:
|
||||
type: string
|
||||
pisa:
|
||||
type: string
|
||||
provUmum:
|
||||
properties:
|
||||
kdProvider:
|
||||
type: string
|
||||
nmProvider:
|
||||
type: string
|
||||
type: object
|
||||
sex:
|
||||
type: string
|
||||
statusPeserta:
|
||||
properties:
|
||||
keterangan:
|
||||
type: string
|
||||
kode:
|
||||
type: string
|
||||
type: object
|
||||
tglCetakKartu:
|
||||
type: string
|
||||
tglLahir:
|
||||
type: string
|
||||
tglTAT:
|
||||
type: string
|
||||
tglTMT:
|
||||
type: string
|
||||
umur:
|
||||
properties:
|
||||
umurSaatPelayanan:
|
||||
type: string
|
||||
umurSekarang:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
internal_handlers_vclaim_peserta.PesertaResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/internal_handlers_vclaim_peserta.PesertaData'
|
||||
message:
|
||||
type: string
|
||||
metaData: {}
|
||||
request_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
sql.NullString:
|
||||
properties:
|
||||
string:
|
||||
@@ -675,138 +361,6 @@ info:
|
||||
title: API Service
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/Peserta/nik/nik/:nik/tglSEP/:tglsep:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get participant eligibility information by NIK
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: nik
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: nik
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved PesertaBynik data
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handlers_vclaim_peserta.PesertaResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - PesertaBynik not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get PesertaBynik data
|
||||
tags:
|
||||
- Peserta
|
||||
/Rujukan/:norujukan:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manage rujukan
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: norujukan
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: norujukan
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved RujukanBynorujukan data
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - RujukanBynorujukan not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get RujukanBynorujukan data
|
||||
tags:
|
||||
- Rujukan
|
||||
/Rujukan/Peserta/:nokartu:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manage rujukan
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: nokartu
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: nokartu
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved RujukanBynokartu data
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - RujukanBynokartu not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get RujukanBynokartu data
|
||||
tags:
|
||||
- Rujukan
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
@@ -930,6 +484,110 @@ paths:
|
||||
summary: Register new user
|
||||
tags:
|
||||
- Authentication
|
||||
/api/v1/qris/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a single qris by ID
|
||||
parameters:
|
||||
- description: Qris ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Success response
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_qris.QrisGetByIDResponse'
|
||||
"400":
|
||||
description: Invalid ID format
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
|
||||
"404":
|
||||
description: Qris not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
|
||||
summary: Get Qris by ID
|
||||
tags:
|
||||
- Qris
|
||||
/api/v1/qriss:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a paginated list of qriss with optional summary statistics
|
||||
parameters:
|
||||
- default: 10
|
||||
description: Limit (max 100)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
- default: 0
|
||||
description: Offset
|
||||
in: query
|
||||
name: offset
|
||||
type: integer
|
||||
- default: false
|
||||
description: Include aggregation summary
|
||||
in: query
|
||||
name: include_summary
|
||||
type: boolean
|
||||
- description: Filter by status
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
- description: Search in multiple fields
|
||||
in: query
|
||||
name: search
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Success response
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_qris.QrisGetResponse'
|
||||
"400":
|
||||
description: Bad request
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
|
||||
summary: Get qris with pagination and optional aggregation
|
||||
tags:
|
||||
- Qris
|
||||
/api/v1/qriss/stats:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns comprehensive statistics about qris data
|
||||
parameters:
|
||||
- description: Filter statistics by status
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Statistics data
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.AggregateData'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
|
||||
summary: Get qris statistics
|
||||
tags:
|
||||
- Qris
|
||||
/api/v1/retribusi/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
@@ -1252,230 +910,6 @@ paths:
|
||||
summary: Generate token directly
|
||||
tags:
|
||||
- Token
|
||||
/peserta/nokartu/:nokartu/tglSEP/:tglsep:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get participant eligibility information by card number
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: nokartu
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: nokartu
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved PesertaBynokartu data
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handlers_vclaim_peserta.PesertaResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - PesertaBynokartu not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get PesertaBynokartu data
|
||||
tags:
|
||||
- Peserta
|
||||
/sep:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manage SEP (Surat Eligibilitas Peserta)
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: SepSep data
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Successfully created SepSep
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
|
||||
"400":
|
||||
description: Bad request - invalid request body or validation error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"409":
|
||||
description: Conflict - SepSep already exists
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Create new SepSep
|
||||
tags:
|
||||
- Sep
|
||||
/sep/:nosep:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manage SEP (Surat Eligibilitas Peserta)
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: nosep
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: nosep
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully deleted SepSep
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - SepSep not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Delete existing SepSep
|
||||
tags:
|
||||
- Sep
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manage SEP (Surat Eligibilitas Peserta)
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: nosep
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: nosep
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved SepSep data
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - SepSep not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get SepSep data
|
||||
tags:
|
||||
- Sep
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Manage SEP (Surat Eligibilitas Peserta)
|
||||
parameters:
|
||||
- description: Request ID for tracking
|
||||
in: header
|
||||
name: X-Request-ID
|
||||
type: string
|
||||
- description: nosep
|
||||
example: '"example_value"'
|
||||
in: path
|
||||
name: nosep
|
||||
required: true
|
||||
type: string
|
||||
- description: SepSep data
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully updated SepSep
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
|
||||
"400":
|
||||
description: Bad request - invalid parameters or request body
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"401":
|
||||
description: Unauthorized - invalid API credentials
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"404":
|
||||
description: Not found - SepSep not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Update existing SepSep
|
||||
tags:
|
||||
- Sep
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
services "api-service/internal/services/satusehat"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("SatuSehat Specific Requests - Organization & Patient by NIK")
|
||||
fmt.Println("==========================================================")
|
||||
|
||||
// Load environment variables from .env file
|
||||
err := godotenv.Load("../.env")
|
||||
if err != nil {
|
||||
log.Printf("Warning: Could not load .env file: %v", err)
|
||||
}
|
||||
|
||||
// Set debug logging
|
||||
os.Setenv("LOG_LEVEL", "debug")
|
||||
|
||||
// Load configuration from environment variables
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
// Create SatuSehat service
|
||||
service := services.NewSatuSehatServiceFromConfig(cfg)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Test 1: Get Organization by ID (using OrgID from config)
|
||||
fmt.Println("\n1. Get Organization by ID:")
|
||||
orgID := cfg.SatuSehat.OrgID
|
||||
fmt.Printf(" Using OrgID: %s\n", orgID)
|
||||
|
||||
orgEndpoint := fmt.Sprintf("/Organization/%s", orgID)
|
||||
orgResult, err := service.GetRawResponse(ctx, orgEndpoint)
|
||||
if err != nil {
|
||||
log.Printf("Error getting organization: %v", err)
|
||||
} else {
|
||||
fmt.Printf(" Status Code: %d\n", orgResult.StatusCode)
|
||||
fmt.Printf(" Success: %v\n", orgResult.Success)
|
||||
fmt.Printf(" Message: %s\n", orgResult.Message)
|
||||
|
||||
if orgResult.Data != nil {
|
||||
fmt.Printf(" Data Type: %T\n", orgResult.Data)
|
||||
|
||||
// Convert data to JSON for better readability
|
||||
if jsonData, err := json.MarshalIndent(orgResult.Data, " ", " "); err == nil {
|
||||
fmt.Printf(" Organization Data: \n%s\n", string(jsonData))
|
||||
} else {
|
||||
fmt.Printf(" Data (raw): %+v\n", orgResult.Data)
|
||||
}
|
||||
}
|
||||
|
||||
if orgResult.Error != nil {
|
||||
fmt.Printf(" Error Code: %s\n", orgResult.Error.Code)
|
||||
fmt.Printf(" Error Details: %s\n", orgResult.Error.Details)
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Get Patient by specific NIK
|
||||
fmt.Println("\n2. Get Patient by Specific NIK:")
|
||||
specificNIK := "3512162601960002"
|
||||
fmt.Printf(" Using NIK: %s\n", specificNIK)
|
||||
|
||||
patientResult, err := service.GetPatientByNIK(ctx, specificNIK)
|
||||
if err != nil {
|
||||
log.Printf("Error getting patient: %v", err)
|
||||
} else {
|
||||
fmt.Printf(" Status Code: %d\n", patientResult.StatusCode)
|
||||
fmt.Printf(" Success: %v\n", patientResult.Success)
|
||||
fmt.Printf(" Message: %s\n", patientResult.Message)
|
||||
|
||||
if patientResult.Data != nil {
|
||||
fmt.Printf(" Data Type: %T\n", patientResult.Data)
|
||||
|
||||
// Convert data to JSON for better readability
|
||||
if jsonData, err := json.MarshalIndent(patientResult.Data, " ", " "); err == nil {
|
||||
fmt.Printf(" Patient Data: \n%s\n", string(jsonData))
|
||||
|
||||
// Extract and display specific patient information
|
||||
displayPatientDetails(patientResult.Data)
|
||||
} else {
|
||||
fmt.Printf(" Data (raw): %+v\n", patientResult.Data)
|
||||
}
|
||||
}
|
||||
|
||||
if patientResult.Error != nil {
|
||||
fmt.Printf(" Error Code: %s\n", patientResult.Error.Code)
|
||||
fmt.Printf(" Error Details: %s\n", patientResult.Error.Details)
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Health check to verify token status
|
||||
fmt.Println("\n3. Service Health Check:")
|
||||
isValid := service.IsTokenValid()
|
||||
fmt.Printf(" Token Valid: %v\n", isValid)
|
||||
|
||||
if !isValid {
|
||||
fmt.Println(" Refreshing token...")
|
||||
err = service.RefreshToken(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error refreshing token: %v", err)
|
||||
} else {
|
||||
fmt.Println(" Token refreshed successfully")
|
||||
fmt.Printf(" Token Valid After Refresh: %v\n", service.IsTokenValid())
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nSpecific requests test completed!")
|
||||
}
|
||||
|
||||
// displayPatientDetails extracts and displays specific patient information from FHIR response
|
||||
func displayPatientDetails(data interface{}) {
|
||||
fmt.Println("\n --- Patient Details ---")
|
||||
|
||||
// Convert to map for easier access
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
// Check if it's a Bundle
|
||||
if resourceType, exists := dataMap["resourceType"]; exists && resourceType == "Bundle" {
|
||||
if entries, exists := dataMap["entry"]; exists {
|
||||
if entryList, ok := entries.([]interface{}); ok && len(entryList) > 0 {
|
||||
if firstEntry, ok := entryList[0].(map[string]interface{}); ok {
|
||||
if resource, exists := firstEntry["resource"]; exists {
|
||||
if patient, ok := resource.(map[string]interface{}); ok {
|
||||
// Display basic patient info
|
||||
fmt.Printf(" Patient ID: %v\n", patient["id"])
|
||||
|
||||
// Display name
|
||||
if names, exists := patient["name"].([]interface{}); exists && len(names) > 0 {
|
||||
if name, ok := names[0].(map[string]interface{}); ok {
|
||||
fmt.Printf(" Name: %v\n", name["text"])
|
||||
}
|
||||
}
|
||||
|
||||
// Display identifiers
|
||||
if identifiers, exists := patient["identifier"].([]interface{}); exists {
|
||||
fmt.Println(" Identifiers:")
|
||||
for _, ident := range identifiers {
|
||||
if identifier, ok := ident.(map[string]interface{}); ok {
|
||||
system := identifier["system"]
|
||||
value := identifier["value"]
|
||||
fmt.Printf(" - %s: %v\n", system, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display status
|
||||
fmt.Printf(" Active: %v\n", patient["active"])
|
||||
|
||||
// Display meta information
|
||||
if meta, exists := patient["meta"].(map[string]interface{}); exists {
|
||||
fmt.Printf(" Last Updated: %v\n", meta["lastUpdated"])
|
||||
if profiles, exists := meta["profile"].([]interface{}); exists {
|
||||
fmt.Printf(" FHIR Profile: %v\n", profiles[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(" -----------------------")
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (c *Config) loadDatabaseConfigs() {
|
||||
Port: getEnvAsInt("DB_PORT", 5432),
|
||||
Username: getEnv("DB_USERNAME", ""),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
Database: getEnv("DB_DATABASE", "satu_db"),
|
||||
Database: getEnv("DB_DATABASE", "simrs_backup"),
|
||||
Schema: getEnv("DB_SCHEMA", "public"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
|
||||
@@ -669,7 +669,7 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bpjs.BaseURL == "" {
|
||||
/*if c.Bpjs.BaseURL == "" {
|
||||
log.Fatal("BPJS Base URL is required")
|
||||
}
|
||||
if c.Bpjs.ConsID == "" {
|
||||
@@ -680,7 +680,7 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
if c.Bpjs.SecretKey == "" {
|
||||
log.Fatal("BPJS Secret Key is required")
|
||||
}
|
||||
}*/
|
||||
|
||||
// Validate Keycloak configuration if enabled
|
||||
if c.Keycloak.Enabled {
|
||||
@@ -696,7 +696,7 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
|
||||
// Validate SatuSehat configuration
|
||||
if c.SatuSehat.OrgID == "" {
|
||||
/*if c.SatuSehat.OrgID == "" {
|
||||
log.Fatal("SatuSehat Organization ID is required")
|
||||
}
|
||||
if c.SatuSehat.FasyakesID == "" {
|
||||
@@ -713,7 +713,7 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
if c.SatuSehat.BaseURL == "" {
|
||||
log.Fatal("SatuSehat Base URL is required")
|
||||
}
|
||||
}*/
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
680
internal/handlers/qris/qris.go
Normal file
680
internal/handlers/qris/qris.go
Normal file
@@ -0,0 +1,680 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
models "api-service/internal/models"
|
||||
"api-service/internal/models/qris"
|
||||
"api-service/internal/utils/validation"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var (
|
||||
qrisdb database.Service
|
||||
qrisonce sync.Once
|
||||
qrisvalidate *validator.Validate
|
||||
)
|
||||
|
||||
// Initialize the database connection and validator
|
||||
func init() {
|
||||
qrisonce.Do(func() {
|
||||
qrisdb = database.New(config.LoadConfig())
|
||||
qrisvalidate = validator.New()
|
||||
qrisvalidate.RegisterValidation("qris_status", validateQrisStatus)
|
||||
if qrisdb == nil {
|
||||
log.Fatal("Failed to initialize database connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Custom validation for qris status
|
||||
func validateQrisStatus(fl validator.FieldLevel) bool {
|
||||
return models.IsValidStatus(fl.Field().String())
|
||||
}
|
||||
|
||||
// QrisHandler handles qris services
|
||||
type QrisHandler struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewQrisHandler creates a new QrisHandler
|
||||
func NewQrisHandler() *QrisHandler {
|
||||
return &QrisHandler{
|
||||
db: qrisdb,
|
||||
}
|
||||
}
|
||||
|
||||
// GetQris godoc
|
||||
// @Summary Get qris with pagination and optional aggregation
|
||||
// @Description Returns a paginated list of qriss with optional summary statistics
|
||||
// @Tags Qris
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit (max 100)" default(10)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Param include_summary query bool false "Include aggregation summary" default(false)
|
||||
// @Param status query string false "Filter by status"
|
||||
// @Param search query string false "Search in multiple fields"
|
||||
// @Success 200 {object} qris.QrisGetResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/qriss [get]
|
||||
func (h *QrisHandler) GetQris(c *gin.Context) {
|
||||
// Parse pagination parameters
|
||||
limit, offset, err := h.parsePaginationParams(c)
|
||||
if err != nil {
|
||||
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse filter parameters
|
||||
filter := h.parseFilterParams(c)
|
||||
includeAggregation := c.Query("include_summary") == "true"
|
||||
|
||||
// Get database connection
|
||||
dbConn, err := h.db.GetDB("simrs_backup")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Execute concurrent operations
|
||||
var (
|
||||
items []qris.Qris
|
||||
total int
|
||||
aggregateData *models.AggregateData
|
||||
wg sync.WaitGroup
|
||||
errChan = make(chan error, 3)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// Fetch total count
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
|
||||
mu.Lock()
|
||||
errChan <- fmt.Errorf("failed to get total count: %w", err)
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Fetch main data
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := h.fetchQriss(ctx, dbConn, filter, limit, offset)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to fetch data: %w", err)
|
||||
} else {
|
||||
items = result
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Fetch aggregation data if requested
|
||||
if includeAggregation {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := h.getAggregateData(ctx, dbConn, filter)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
|
||||
} else {
|
||||
aggregateData = result
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
meta := h.calculateMeta(limit, offset, total)
|
||||
response := qris.QrisGetResponse{
|
||||
Message: "Data qris berhasil diambil",
|
||||
Data: items,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
if includeAggregation && aggregateData != nil {
|
||||
response.Summary = aggregateData
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetQrisByID godoc
|
||||
// @Summary Get Qris by ID
|
||||
// @Description Returns a single qris by ID
|
||||
// @Tags Qris
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Qris ID (UUID)"
|
||||
// @Success 200 {object} qris.QrisGetByIDResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||
// @Failure 404 {object} models.ErrorResponse "Qris not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/qris/{id} [get]
|
||||
func (h *QrisHandler) GetQrisByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
// Validate UUID format
|
||||
intID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetDB("simrs_backup")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
item, err := h.getQrisByID(ctx, dbConn, intID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
h.respondError(c, "Qris not found", err, http.StatusNotFound)
|
||||
} else {
|
||||
h.logAndRespondError(c, "Failed to get qris", err, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response := qris.QrisGetByIDResponse{
|
||||
Message: "qris details retrieved successfully",
|
||||
Data: item,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetQrisStats godoc
|
||||
// @Summary Get qris statistics
|
||||
// @Description Returns comprehensive statistics about qris data
|
||||
// @Tags Qris
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status query string false "Filter statistics by status"
|
||||
// @Success 200 {object} models.AggregateData "Statistics data"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/qriss/stats [get]
|
||||
func (h *QrisHandler) GetQrisStats(c *gin.Context) {
|
||||
dbConn, err := h.db.GetDB("simrs_backup")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
filter := h.parseFilterParams(c)
|
||||
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Statistik qris berhasil diambil",
|
||||
"data": aggregateData,
|
||||
})
|
||||
}
|
||||
|
||||
// Database operations
|
||||
func (h *QrisHandler) getQrisByID(ctx context.Context, dbConn *sql.DB, id int) (*qris.Qris, error) {
|
||||
query := "SELECT id, status, created_at, updated_at, display_name FROM t_qrdata WHERE id = $1 AND status IS NOT NULL"
|
||||
row := dbConn.QueryRowContext(ctx, query, id)
|
||||
|
||||
var item qris.Qris
|
||||
err := row.Scan(&item.ID, &item.Status, &item.CreatedAt, &item.UpdatedAt, &item.DisplayName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
/*func (h *QrisHandler) createQris(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) (*qris.Qris, error) {
|
||||
id := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
query := "INSERT INTO data_qris_qris (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
|
||||
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
|
||||
|
||||
var item qris.Qris
|
||||
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create qris: %w", err)
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}*/
|
||||
|
||||
/*func (h *QrisHandler) updateQris(ctx context.Context, dbConn *sql.DB, req *qris.QrisUpdateRequest) (*qris.Qris, error) {
|
||||
now := time.Now()
|
||||
|
||||
query := "UPDATE data_qris_qris SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
|
||||
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
|
||||
|
||||
var item qris.Qris
|
||||
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update qris: %w", err)
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}*/
|
||||
|
||||
func (h *QrisHandler) deleteQris(ctx context.Context, dbConn *sql.DB, id string) error {
|
||||
now := time.Now()
|
||||
query := "UPDATE data_qris_qris SET status = 'deleted', updated_at = $2 WHERE id = $1 AND status != 'deleted'"
|
||||
|
||||
result, err := dbConn.ExecContext(ctx, query, id, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete qris: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *QrisHandler) fetchQriss(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter, limit, offset int) ([]qris.Qris, error) {
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
query := fmt.Sprintf("SELECT id, status, created_at, updated_at, display_name FROM t_qrdata WHERE %s ORDER BY created_at DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := dbConn.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch qriss query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]qris.Qris, 0, limit)
|
||||
for rows.Next() {
|
||||
item, err := h.scanQris(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan Qris failed: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully fetched %d qriss with filters applied", len(items))
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Optimized scanning function
|
||||
func (h *QrisHandler) scanQris(rows *sql.Rows) (qris.Qris, error) {
|
||||
var item qris.Qris
|
||||
|
||||
// Scan into individual fields to handle nullable types properly
|
||||
err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.Status,
|
||||
&item.CreatedAt, //.Time, &item.CreatedAt.Valid, // sql.NullTime
|
||||
&item.UpdatedAt, //.Time, &item.UpdatedAt.Valid, // sql.NullTime
|
||||
&item.DisplayName, //.String, &item.DisplayName.Valid, // sql.NullString
|
||||
)
|
||||
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (h *QrisHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter, total *int) error {
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM t_qrdata WHERE %s", whereClause)
|
||||
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
|
||||
return fmt.Errorf("total count query failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get comprehensive aggregate data dengan filter support
|
||||
func (h *QrisHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter) (*models.AggregateData, error) {
|
||||
aggregate := &models.AggregateData{
|
||||
ByStatus: make(map[string]int),
|
||||
}
|
||||
|
||||
// Build where clause untuk filter
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
|
||||
// Use concurrent execution untuk performance
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
// 1. Count by status
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM t_qrdata WHERE %s GROUP BY status ORDER BY status", whereClause)
|
||||
|
||||
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("status query failed: %w", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
mu.Lock()
|
||||
for rows.Next() {
|
||||
var status string
|
||||
var count int
|
||||
if err := rows.Scan(&status, &count); err != nil {
|
||||
mu.Unlock()
|
||||
errChan <- fmt.Errorf("status scan failed: %w", err)
|
||||
return
|
||||
}
|
||||
aggregate.ByStatus[status] = count
|
||||
switch status {
|
||||
case "active":
|
||||
aggregate.TotalActive = count
|
||||
case "draft":
|
||||
aggregate.TotalDraft = count
|
||||
case "inactive":
|
||||
aggregate.TotalInactive = count
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
errChan <- fmt.Errorf("status iteration error: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. Get last updated time dan today statistics
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Last updated
|
||||
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(updated_at) FROM t_qrdata WHERE %s AND updated_at IS NOT NULL", whereClause)
|
||||
var lastUpdated sql.NullTime
|
||||
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
|
||||
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Today statistics
|
||||
today := time.Now().Format("2006-01-02")
|
||||
todayStatsQuery := fmt.Sprintf(`
|
||||
SELECT
|
||||
SUM(CASE WHEN DATE(created_at) = $%d THEN 1 ELSE 0 END) as created_today,
|
||||
SUM(CASE WHEN DATE(updated_at) = $%d AND DATE(created_at) != $%d THEN 1 ELSE 0 END) as updated_today
|
||||
FROM t_qrdata
|
||||
WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause)
|
||||
|
||||
todayArgs := append(args, today)
|
||||
var createdToday, updatedToday int
|
||||
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
|
||||
errChan <- fmt.Errorf("today stats query failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
if lastUpdated.Valid {
|
||||
aggregate.LastUpdated = &lastUpdated.Time
|
||||
}
|
||||
aggregate.CreatedToday = createdToday
|
||||
aggregate.UpdatedToday = updatedToday
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Wait for all goroutines
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
// Enhanced error handling
|
||||
func (h *QrisHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
log.Printf("[ERROR] %s: %v", message, err)
|
||||
h.respondError(c, message, err, statusCode)
|
||||
}
|
||||
|
||||
func (h *QrisHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
errorMessage := message
|
||||
if gin.Mode() == gin.ReleaseMode {
|
||||
errorMessage = "Internal server error"
|
||||
}
|
||||
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Error: errorMessage,
|
||||
Code: statusCode,
|
||||
Message: err.Error(),
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse pagination parameters dengan validation yang lebih ketat
|
||||
func (h *QrisHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
|
||||
limit := 10 // Default limit
|
||||
offset := 0 // Default offset
|
||||
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
parsedLimit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
|
||||
}
|
||||
if parsedLimit <= 0 {
|
||||
return 0, 0, fmt.Errorf("limit must be greater than 0")
|
||||
}
|
||||
if parsedLimit > 100 {
|
||||
return 0, 0, fmt.Errorf("limit cannot exceed 100")
|
||||
}
|
||||
limit = parsedLimit
|
||||
}
|
||||
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
parsedOffset, err := strconv.Atoi(offsetStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
|
||||
}
|
||||
if parsedOffset < 0 {
|
||||
return 0, 0, fmt.Errorf("offset cannot be negative")
|
||||
}
|
||||
offset = parsedOffset
|
||||
}
|
||||
|
||||
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
|
||||
return limit, offset, nil
|
||||
}
|
||||
|
||||
func (h *QrisHandler) parseFilterParams(c *gin.Context) qris.QrisFilter {
|
||||
filter := qris.QrisFilter{}
|
||||
|
||||
if status := c.Query("status"); status != "" {
|
||||
if models.IsValidStatus(status) {
|
||||
filter.Status = &status
|
||||
}
|
||||
}
|
||||
|
||||
if search := c.Query("search"); search != "" {
|
||||
filter.Search = &search
|
||||
}
|
||||
|
||||
// Parse date filters
|
||||
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
|
||||
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
|
||||
filter.DateFrom = &dateFrom
|
||||
}
|
||||
}
|
||||
|
||||
if dateToStr := c.Query("date_to"); dateToStr != "" {
|
||||
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
|
||||
filter.DateTo = &dateTo
|
||||
}
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// Build WHERE clause dengan filter parameters
|
||||
func (h *QrisHandler) buildWhereClause(filter qris.QrisFilter) (string, []interface{}) {
|
||||
conditions := []string{"status IS NOT NULL"}
|
||||
args := []interface{}{}
|
||||
paramCount := 1
|
||||
|
||||
if filter.Status != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
|
||||
args = append(args, *filter.Status)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
if filter.Search != nil {
|
||||
searchCondition := fmt.Sprintf("display_name ILIKE $%d", paramCount)
|
||||
conditions = append(conditions, searchCondition)
|
||||
searchTerm := "%" + *filter.Search + "%"
|
||||
args = append(args, searchTerm)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
if filter.DateFrom != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", paramCount))
|
||||
args = append(args, *filter.DateFrom)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
if filter.DateTo != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("created_at <= $%d", paramCount))
|
||||
args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
|
||||
paramCount++
|
||||
}
|
||||
|
||||
return strings.Join(conditions, " AND "), args
|
||||
}
|
||||
|
||||
func (h *QrisHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
||||
totalPages := 0
|
||||
currentPage := 1
|
||||
if limit > 0 {
|
||||
totalPages = (total + limit - 1) / limit // Ceiling division
|
||||
currentPage = (offset / limit) + 1
|
||||
}
|
||||
|
||||
return models.MetaResponse{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
CurrentPage: currentPage,
|
||||
HasNext: offset+limit < total,
|
||||
HasPrev: offset > 0,
|
||||
}
|
||||
}
|
||||
|
||||
// validateQrisSubmission performs validation for duplicate entries and daily submission limits
|
||||
/*func (h *QrisHandler) validateQrisSubmission(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) error {
|
||||
// Import the validation utility
|
||||
validator := validation.NewDuplicateValidator(dbConn)
|
||||
|
||||
// Use default configuration
|
||||
config := validation.ValidationConfig{
|
||||
TableName: "data_qris_qris",
|
||||
IDColumn: "id",
|
||||
StatusColumn: "status",
|
||||
DateColumn: "date_created",
|
||||
ActiveStatuses: []string{"active", "draft"},
|
||||
}
|
||||
|
||||
// Validate duplicate entries with active status for today
|
||||
err := validator.ValidateDuplicate(ctx, config, "dummy_id")
|
||||
if err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Validate once per day submission
|
||||
err = validator.ValidateOncePerDay(ctx, "data_qris_qris", "id", "date_created", "daily_limit")
|
||||
if err != nil {
|
||||
return fmt.Errorf("daily submission limit exceeded: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}*/
|
||||
|
||||
// Example usage of the validation utility with custom configuration
|
||||
/*func (h *QrisHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) error {
|
||||
// Create validator instance
|
||||
validator := validation.NewDuplicateValidator(dbConn)
|
||||
|
||||
// Use custom configuration
|
||||
config := validation.ValidationConfig{
|
||||
TableName: "data_qris_qris",
|
||||
IDColumn: "id",
|
||||
StatusColumn: "status",
|
||||
DateColumn: "date_created",
|
||||
ActiveStatuses: []string{"active", "draft"},
|
||||
AdditionalFields: map[string]interface{}{
|
||||
"name": req.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate with custom fields
|
||||
fields := map[string]interface{}{
|
||||
"name": *req.Name,
|
||||
}
|
||||
|
||||
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
||||
if err != nil {
|
||||
return fmt.Errorf("custom validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}*/
|
||||
|
||||
// GetLastSubmissionTime example
|
||||
func (h *QrisHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
|
||||
validator := validation.NewDuplicateValidator(dbConn)
|
||||
return validator.GetLastSubmissionTime(ctx, "t_qrdata", "id", "created_at", identifier)
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
// Service: VClaim (vclaim)
|
||||
// Description: BPJS VClaim service for eligibility and SEP management
|
||||
|
||||
package peserta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/models"
|
||||
"api-service/internal/models/vclaim/peserta"
|
||||
services "api-service/internal/services/bpjs"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// VClaimHandler handles VClaim BPJS services
|
||||
type VClaimHandler struct {
|
||||
service services.VClaimService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
config config.BpjsConfig
|
||||
}
|
||||
|
||||
// VClaimHandlerConfig contains configuration for VClaimHandler
|
||||
type VClaimHandlerConfig struct {
|
||||
BpjsConfig config.BpjsConfig
|
||||
Logger logger.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewVClaimHandler creates a new VClaimHandler
|
||||
func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler {
|
||||
return &VClaimHandler{
|
||||
service: services.NewService(cfg.BpjsConfig),
|
||||
validator: cfg.Validator,
|
||||
logger: cfg.Logger,
|
||||
config: cfg.BpjsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPesertaBynokartu godoc
|
||||
// @Summary Get PesertaBynokartu data
|
||||
// @Description Get participant eligibility information by card number
|
||||
// @Tags Peserta
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nokartu path string true "nokartu" example("example_value")
|
||||
// @Success 200 {object} peserta.PesertaResponse "Successfully retrieved PesertaBynokartu data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - PesertaBynokartu not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /peserta/nokartu/:nokartu/tglSEP/:tglsep [get]
|
||||
func (h *VClaimHandler) GetPesertaBynokartu(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
h.logger.Info("Processing GetPesertaBynokartu request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"nokartu": c.Param("nokartu"),
|
||||
"tglsep": c.Param("tglsep"),
|
||||
})
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
nokartu := c.Param("nokartu")
|
||||
tglsep := c.Param("tglsep")
|
||||
if nokartu == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nokartu",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response peserta.PesertaResponse
|
||||
|
||||
endpoint := "/Peserta/nokartu/:nokartu/tglSEP/:tglsep"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
|
||||
endpoint = strings.Replace(endpoint, ":tglsep", tglsep, 1)
|
||||
|
||||
resp, err := h.service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &peserta.PesertaData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if pesertaMap, exists := respMap["peserta"]; exists {
|
||||
pesertaBytes, _ := json.Marshal(pesertaMap)
|
||||
json.Unmarshal(pesertaBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetPesertaBynik godoc
|
||||
// @Summary Get PesertaBynik data
|
||||
// @Description Get participant eligibility information by NIK
|
||||
// @Tags Peserta
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nik path string true "nik" example("example_value")
|
||||
// @Success 200 {object} peserta.PesertaResponse "Successfully retrieved PesertaBynik data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - PesertaBynik not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Peserta/nik/nik/:nik/tglSEP/:tglsep [get]
|
||||
func (h *VClaimHandler) GetPesertaBynik(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
h.logger.Info("Processing GetPesertaBynik request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/peserta/nik/:nik/tglSEP/:tglsep",
|
||||
|
||||
"nik": c.Param("nik"),
|
||||
})
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
nik := c.Param("nik")
|
||||
tglsep := c.Param("tglsep")
|
||||
if nik == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nik", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nik",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if tglsep == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter Tanggal SEP", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter Tanggal SEP",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
// Call service method
|
||||
var response peserta.PesertaResponse
|
||||
|
||||
endpoint := "/Peserta/nik/:nik/tglSEP/:tglsep"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nik", nik, 1)
|
||||
endpoint = strings.Replace(endpoint, ":tglsep", tglsep, 1)
|
||||
|
||||
resp, err := h.service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get PesertaBynik", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &peserta.PesertaData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if pesertaMap, exists := respMap["peserta"]; exists {
|
||||
pesertaBytes, _ := json.Marshal(pesertaMap)
|
||||
json.Unmarshal(pesertaBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
// Service: VClaim (vclaim)
|
||||
// Description: BPJS VClaim service for eligibility and SEP management
|
||||
|
||||
package rujukan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/models"
|
||||
"api-service/internal/models/vclaim/rujukan"
|
||||
services "api-service/internal/services/bpjs"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// VClaimHandler handles VClaim BPJS services
|
||||
type VClaimHandler struct {
|
||||
service services.VClaimService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
config config.BpjsConfig
|
||||
}
|
||||
|
||||
// VClaimHandlerConfig contains configuration for VClaimHandler
|
||||
type VClaimHandlerConfig struct {
|
||||
BpjsConfig config.BpjsConfig
|
||||
Logger logger.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewVClaimHandler creates a new VClaimHandler
|
||||
func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler {
|
||||
return &VClaimHandler{
|
||||
service: services.NewService(cfg.BpjsConfig),
|
||||
validator: cfg.Validator,
|
||||
logger: cfg.Logger,
|
||||
config: cfg.BpjsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRujukanBynorujukan godoc
|
||||
// @Summary Get RujukanBynorujukan data
|
||||
// @Description Manage rujukan
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param norujukan path string true "norujukan" example("example_value")
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully retrieved RujukanBynorujukan data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - RujukanBynorujukan not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukan/:norujukan [get]
|
||||
func (h *VClaimHandler) GetRujukanBynorujukan(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
h.logger.Info("Processing GetRujukanBynorujukan request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan/:norujukan",
|
||||
|
||||
"norujukan": c.Param("norujukan"),
|
||||
})
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
norujukan := c.Param("norujukan")
|
||||
if norujukan == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter norujukan", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter norujukan",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
endpoint := "/Rujukan/RS/:norujukan"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":norujukan", norujukan, 1)
|
||||
|
||||
resp, err := h.service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if pesertaMap, exists := respMap["rujukan"]; exists {
|
||||
pesertaBytes, _ := json.Marshal(pesertaMap)
|
||||
json.Unmarshal(pesertaBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetRujukanBynokartu godoc
|
||||
// @Summary Get RujukanBynokartu data
|
||||
// @Description Manage rujukan
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nokartu path string true "nokartu" example("example_value")
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully retrieved RujukanBynokartu data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - RujukanBynokartu not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukan/Peserta/:nokartu [get]
|
||||
func (h *VClaimHandler) GetRujukanBynokartu(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
h.logger.Info("Processing GetRujukanBynokartu request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan/Peserta/:nokartu",
|
||||
|
||||
"nokartu": c.Param("nokartu"),
|
||||
})
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
nokartu := c.Param("nokartu")
|
||||
if nokartu == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nokartu",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
endpoint := "/Rujukan/RS/Peserta/:nokartu"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
|
||||
|
||||
resp, err := h.service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if pesertaMap, exists := respMap["rujukan"]; exists {
|
||||
pesertaBytes, _ := json.Marshal(pesertaMap)
|
||||
json.Unmarshal(pesertaBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
|
||||
// Service: VClaim (vclaim)
|
||||
// Description: BPJS VClaim service for eligibility and SEP management
|
||||
|
||||
package sep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/models"
|
||||
"api-service/internal/models/vclaim/sep"
|
||||
"api-service/internal/services/bpjs"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// VClaimHandler handles VClaim BPJS services
|
||||
type VClaimHandler struct {
|
||||
service services.VClaimService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
config config.BpjsConfig
|
||||
}
|
||||
|
||||
// VClaimHandlerConfig contains configuration for VClaimHandler
|
||||
type VClaimHandlerConfig struct {
|
||||
BpjsConfig config.BpjsConfig
|
||||
Logger logger.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
// NewVClaimHandler creates a new VClaimHandler
|
||||
func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler {
|
||||
return &VClaimHandler{
|
||||
service: services.NewService(cfg.BpjsConfig),
|
||||
validator: cfg.Validator,
|
||||
logger: cfg.Logger,
|
||||
config: cfg.BpjsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GetSepSep godoc
|
||||
// @Summary Get SepSep data
|
||||
// @Description Manage SEP (Surat Eligibilitas Peserta)
|
||||
// @Tags Sep
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nosep path string true "nosep" example("example_value")
|
||||
// @Success 200 {object} sep.SepResponse "Successfully retrieved SepSep data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - SepSep not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /sep/:nosep [get]
|
||||
func (h *VClaimHandler) GetSepSep(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing GetSepSep request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/sep/:nosep",
|
||||
|
||||
"nosep": c.Param("nosep"),
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
nosep := c.Param("nosep")
|
||||
if nosep == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nosep", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nosep",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Call service method
|
||||
var response sep.SepResponse
|
||||
|
||||
endpoint := "/sep/:nosep"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nosep", nosep, 1)
|
||||
|
||||
err := h.service.Get(ctx, endpoint, &response)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to get SepSep", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// CreateSepSep godoc
|
||||
// @Summary Create new SepSep
|
||||
// @Description Manage SEP (Surat Eligibilitas Peserta)
|
||||
// @Tags Sep
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param request body sep.SepRequest true "SepSep data"
|
||||
// @Success 201 {object} sep.SepResponse "Successfully created SepSep"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid request body or validation error"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - SepSep already exists"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /sep [post]
|
||||
func (h *VClaimHandler) CreateSepSep(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing CreateSepSep request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
var req sep.SepRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
h.logger.Error("Invalid request body", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
|
||||
h.logger.Error("Validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Validation failed: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response sep.SepResponse
|
||||
err := h.service.Post(ctx, "/sep", &req, &response)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to create SepSep", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// UpdateSepSep godoc
|
||||
// @Summary Update existing SepSep
|
||||
// @Description Manage SEP (Surat Eligibilitas Peserta)
|
||||
// @Tags Sep
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nosep path string true "nosep" example("example_value")
|
||||
// @Param request body sep.SepRequest true "SepSep data"
|
||||
// @Success 200 {object} sep.SepResponse "Successfully updated SepSep"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters or request body"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - SepSep not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /sep/:nosep [put]
|
||||
func (h *VClaimHandler) UpdateSepSep(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing UpdateSepSep request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
|
||||
nosep := c.Param("nosep")
|
||||
if nosep == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nosep", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nosep",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var req sep.SepRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
h.logger.Error("Invalid request body", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
|
||||
h.logger.Error("Validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Validation failed: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response sep.SepResponse
|
||||
|
||||
endpoint := "/sep/:nosep"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nosep", nosep, 1)
|
||||
|
||||
err := h.service.Put(ctx, endpoint, &req, &response)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to update SepSep", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// DeleteSepSep godoc
|
||||
// @Summary Delete existing SepSep
|
||||
// @Description Manage SEP (Surat Eligibilitas Peserta)
|
||||
// @Tags Sep
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nosep path string true "nosep" example("example_value")
|
||||
// @Success 200 {object} sep.SepResponse "Successfully deleted SepSep"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - SepSep not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /sep/:nosep [delete]
|
||||
func (h *VClaimHandler) DeleteSepSep(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing DeleteSepSep request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
|
||||
nosep := c.Param("nosep")
|
||||
if nosep == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nosep", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nosep",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Call service method
|
||||
var response sep.SepResponse
|
||||
|
||||
endpoint := "/sep/:nosep"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nosep", nosep, 1)
|
||||
|
||||
err := h.service.Delete(ctx, endpoint, &response)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to delete SepSep", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
lzstring "github.com/daku10/go-lz-string"
|
||||
)
|
||||
|
||||
// StringDecrypt - langsung coba decompress tanpa decrypt ulang
|
||||
func StringDecrypt(key string, encryptedString string) (string, error) {
|
||||
log.Printf("StringDecrypt: Attempting decompression, data length: %d", len(encryptedString))
|
||||
|
||||
// Method 1: Try direct LZ-string decompression (data sudah didecrypt di response.go)
|
||||
if result, err := lzstring.DecompressFromEncodedURIComponent(encryptedString); err == nil && len(result) > 0 {
|
||||
log.Printf("StringDecrypt: Direct decompression successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 2: Try other LZ-string methods
|
||||
if result, err := lzstring.DecompressFromBase64(encryptedString); err == nil && len(result) > 0 {
|
||||
log.Printf("StringDecrypt: Base64 decompression successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 3: If all fail, return the original string
|
||||
log.Printf("StringDecrypt: All decompression failed, returning original data")
|
||||
return encryptedString, nil
|
||||
}
|
||||
|
||||
func RemovePKCS7Padding(data []byte) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
paddingLength := int(data[len(data)-1])
|
||||
if paddingLength > len(data) || paddingLength == 0 {
|
||||
log.Printf("RemovePKCS7Padding: Invalid padding length: %d, data length: %d", paddingLength, len(data))
|
||||
return data // Return original data if padding is invalid
|
||||
}
|
||||
|
||||
// Verify all padding bytes are correct
|
||||
for i := len(data) - paddingLength; i < len(data); i++ {
|
||||
if data[i] != byte(paddingLength) {
|
||||
log.Printf("RemovePKCS7Padding: Invalid padding byte at position %d", i)
|
||||
return data // Return original data if padding is invalid
|
||||
}
|
||||
}
|
||||
|
||||
return data[:len(data)-paddingLength]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package helper
|
||||
|
||||
import "errors"
|
||||
|
||||
func Pad(buf []byte, size int) ([]byte, error) {
|
||||
bufLen := len(buf)
|
||||
padLen := size - bufLen%size
|
||||
padded := make([]byte, bufLen+padLen)
|
||||
copy(padded, buf)
|
||||
for i := 0; i < padLen; i++ {
|
||||
padded[bufLen+i] = byte(padLen)
|
||||
}
|
||||
return padded, nil
|
||||
}
|
||||
|
||||
func Unpad(padded []byte, size int) ([]byte, error) {
|
||||
if len(padded)%size != 0 {
|
||||
return nil, errors.New("pkcs7: Padded value wasn't in correct size.")
|
||||
}
|
||||
|
||||
bufLen := len(padded) - int(padded[len(padded)-1])
|
||||
buf := make([]byte, bufLen)
|
||||
copy(buf, padded[:bufLen])
|
||||
return buf, nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package aplicare
|
||||
|
||||
import "api-service/internal/models"
|
||||
|
||||
// === MONITORING MODELS ===
|
||||
|
||||
// MonitoringRequest represents monitoring data request
|
||||
type MonitoringRequest struct {
|
||||
models.BaseRequest
|
||||
TanggalAwal string `json:"tanggal_awal" validate:"required"`
|
||||
TanggalAkhir string `json:"tanggal_akhir" validate:"required"`
|
||||
JenisLaporan string `json:"jenis_laporan" validate:"required,oneof=kunjungan klaim rujukan sep"`
|
||||
PPK string `json:"ppk,omitempty"`
|
||||
StatusData string `json:"status_data,omitempty"`
|
||||
models.PaginationRequest
|
||||
}
|
||||
|
||||
// MonitoringData represents monitoring information
|
||||
type MonitoringData struct {
|
||||
Tanggal string `json:"tanggal"`
|
||||
PPK string `json:"ppk"`
|
||||
NamaPPK string `json:"nama_ppk"`
|
||||
JumlahKasus int `json:"jumlah_kasus"`
|
||||
TotalTarif float64 `json:"total_tarif"`
|
||||
StatusData string `json:"status_data"`
|
||||
Keterangan string `json:"keterangan,omitempty"`
|
||||
}
|
||||
|
||||
// MonitoringResponse represents monitoring API response
|
||||
type MonitoringResponse struct {
|
||||
models.BaseResponse
|
||||
Data []MonitoringData `json:"data,omitempty"`
|
||||
Summary *MonitoringSummary `json:"summary,omitempty"`
|
||||
Pagination *models.PaginationResponse `json:"pagination,omitempty"`
|
||||
}
|
||||
|
||||
// MonitoringSummary represents monitoring summary
|
||||
type MonitoringSummary struct {
|
||||
TotalKasus int `json:"total_kasus"`
|
||||
TotalTarif float64 `json:"total_tarif"`
|
||||
RataRataTarif float64 `json:"rata_rata_tarif"`
|
||||
PeriodeLaporan string `json:"periode_laporan"`
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package aplicare
|
||||
|
||||
import "api-service/internal/models"
|
||||
|
||||
// === REFERENSI MODELS ===
|
||||
|
||||
// ReferensiRequest represents referensi lookup request
|
||||
type ReferensiRequest struct {
|
||||
models.BaseRequest
|
||||
JenisReferensi string `json:"jenis_referensi" validate:"required,oneof=diagnosa procedure obat alkes faskes dokter poli"`
|
||||
Keyword string `json:"keyword,omitempty"`
|
||||
KodeReferensi string `json:"kode_referensi,omitempty"`
|
||||
models.PaginationRequest
|
||||
}
|
||||
|
||||
// ReferensiData represents referensi information
|
||||
type ReferensiData struct {
|
||||
Kode string `json:"kode"`
|
||||
Nama string `json:"nama"`
|
||||
Kategori string `json:"kategori,omitempty"`
|
||||
Status string `json:"status"`
|
||||
TglBerlaku string `json:"tgl_berlaku,omitempty"`
|
||||
TglBerakhir string `json:"tgl_berakhir,omitempty"`
|
||||
Keterangan string `json:"keterangan,omitempty"`
|
||||
}
|
||||
|
||||
// ReferensiResponse represents referensi API response
|
||||
type ReferensiResponse struct {
|
||||
models.BaseResponse
|
||||
Data []ReferensiData `json:"data,omitempty"`
|
||||
Pagination *models.PaginationResponse `json:"pagination,omitempty"`
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package eclaim
|
||||
|
||||
import "api-service/internal/models"
|
||||
|
||||
// === KLAIM MODELS ===
|
||||
|
||||
// KlaimRequest represents klaim submission request
|
||||
type KlaimRequest struct {
|
||||
models.BaseRequest
|
||||
NoSep string `json:"nomor_sep" validate:"required"`
|
||||
NoKartu string `json:"nomor_kartu" validate:"required"`
|
||||
NoMR string `json:"nomor_mr" validate:"required"`
|
||||
TglPulang string `json:"tgl_pulang" validate:"required"`
|
||||
TglMasuk string `json:"tgl_masuk" validate:"required"`
|
||||
JnsPelayanan string `json:"jenis_pelayanan" validate:"required,oneof=1 2"`
|
||||
CaraPulang string `json:"cara_pulang" validate:"required"`
|
||||
Data KlaimData `json:"data" validate:"required"`
|
||||
}
|
||||
|
||||
// KlaimData represents detailed klaim information
|
||||
type KlaimData struct {
|
||||
Diagnosa []DiagnosaKlaim `json:"diagnosa" validate:"required,dive"`
|
||||
Procedure []ProcedureKlaim `json:"procedure,omitempty"`
|
||||
Investigasi []InvestigasiKlaim `json:"investigasi,omitempty"`
|
||||
ObatAlkes []ObatKlaim `json:"obat_alkes,omitempty"`
|
||||
TarifRS []TarifKlaim `json:"tarif_rs,omitempty"`
|
||||
DRG *DRGInfo `json:"drg,omitempty"`
|
||||
SpecialCMG *SpecialCMGInfo `json:"special_cmg,omitempty"`
|
||||
}
|
||||
|
||||
// DiagnosaKlaim represents diagnosis in klaim
|
||||
type DiagnosaKlaim struct {
|
||||
KodeDiagnosa string `json:"kode_diagnosa" validate:"required"`
|
||||
NamaDiagnosa string `json:"nama_diagnosa"`
|
||||
TipeDiagnosa string `json:"tipe_diagnosa" validate:"required,oneof=1 2"`
|
||||
}
|
||||
|
||||
// ProcedureKlaim represents procedure in klaim
|
||||
type ProcedureKlaim struct {
|
||||
KodeTindakan string `json:"kode_tindakan" validate:"required"`
|
||||
NamaTindakan string `json:"nama_tindakan"`
|
||||
TanggalTindakan string `json:"tanggal_tindakan" validate:"required"`
|
||||
Keterangan string `json:"keterangan,omitempty"`
|
||||
}
|
||||
|
||||
// InvestigasiKlaim represents investigation/lab results
|
||||
type InvestigasiKlaim struct {
|
||||
KodeInvestigasi string `json:"kode_investigasi" validate:"required"`
|
||||
NamaInvestigasi string `json:"nama_investigasi"`
|
||||
Hasil string `json:"hasil,omitempty"`
|
||||
Satuan string `json:"satuan,omitempty"`
|
||||
NilaiNormal string `json:"nilai_normal,omitempty"`
|
||||
}
|
||||
|
||||
// ObatKlaim represents medication in klaim
|
||||
type ObatKlaim struct {
|
||||
KodeObat string `json:"kode_obat" validate:"required"`
|
||||
NamaObat string `json:"nama_obat"`
|
||||
Dosis string `json:"dosis,omitempty"`
|
||||
Frekuensi string `json:"frekuensi,omitempty"`
|
||||
Jumlah float64 `json:"jumlah" validate:"min=0"`
|
||||
Harga float64 `json:"harga" validate:"min=0"`
|
||||
}
|
||||
|
||||
// TarifKlaim represents hospital tariff
|
||||
type TarifKlaim struct {
|
||||
KodeTarif string `json:"kode_tarif" validate:"required"`
|
||||
NamaTarif string `json:"nama_tarif"`
|
||||
Jumlah int `json:"jumlah" validate:"min=0"`
|
||||
Tarif float64 `json:"tarif" validate:"min=0"`
|
||||
Total float64 `json:"total"`
|
||||
}
|
||||
|
||||
// DRGInfo represents DRG information
|
||||
type DRGInfo struct {
|
||||
KodeDRG string `json:"kode_drg"`
|
||||
NamaDRG string `json:"nama_drg"`
|
||||
TarifDRG float64 `json:"tarif_drg"`
|
||||
Severity string `json:"severity,omitempty"`
|
||||
}
|
||||
|
||||
// SpecialCMGInfo represents Special CMG information
|
||||
type SpecialCMGInfo struct {
|
||||
KodeCMG string `json:"kode_cmg"`
|
||||
NamaCMG string `json:"nama_cmg"`
|
||||
TarifCMG float64 `json:"tarif_cmg"`
|
||||
SubAcute string `json:"sub_acute,omitempty"`
|
||||
}
|
||||
|
||||
// KlaimResponse represents klaim API response
|
||||
type KlaimResponse struct {
|
||||
models.BaseResponse
|
||||
Data *KlaimResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// KlaimResponseData represents processed klaim data
|
||||
type KlaimResponseData struct {
|
||||
NoKlaim string `json:"nomor_klaim"`
|
||||
NoSep string `json:"nomor_sep"`
|
||||
StatusKlaim string `json:"status_klaim"`
|
||||
TarifAktual float64 `json:"tarif_aktual"`
|
||||
TarifRS float64 `json:"tarif_rs"`
|
||||
TarifApproved float64 `json:"tarif_approved"`
|
||||
Grouper *GrouperResult `json:"grouper,omitempty"`
|
||||
}
|
||||
|
||||
// === GROUPER MODELS ===
|
||||
|
||||
// GrouperRequest represents grouper processing request
|
||||
type GrouperRequest struct {
|
||||
models.BaseRequest
|
||||
NoSep string `json:"nomor_sep" validate:"required"`
|
||||
NoKartu string `json:"nomor_kartu" validate:"required"`
|
||||
TglMasuk string `json:"tgl_masuk" validate:"required"`
|
||||
TglPulang string `json:"tgl_pulang" validate:"required"`
|
||||
JnsPelayanan string `json:"jenis_pelayanan" validate:"required,oneof=1 2"`
|
||||
CaraPulang string `json:"cara_pulang" validate:"required"`
|
||||
DiagnosaPrimer string `json:"diagnosa_primer" validate:"required"`
|
||||
DiagnosaSkunder []string `json:"diagnosa_skunder,omitempty"`
|
||||
Procedure []string `json:"procedure,omitempty"`
|
||||
AdlScore int `json:"adl_score,omitempty"`
|
||||
AgeAtAdmission int `json:"age_at_admission" validate:"min=0"`
|
||||
}
|
||||
|
||||
// GrouperResult represents grouper processing result
|
||||
type GrouperResult struct {
|
||||
KodeDRG string `json:"kode_drg"`
|
||||
NamaDRG string `json:"nama_drg"`
|
||||
TarifDRG float64 `json:"tarif_drg"`
|
||||
KodeCMG string `json:"kode_cmg,omitempty"`
|
||||
NamaCMG string `json:"nama_cmg,omitempty"`
|
||||
TarifCMG float64 `json:"tarif_cmg,omitempty"`
|
||||
Severity string `json:"severity"`
|
||||
SubAcute bool `json:"sub_acute"`
|
||||
Chronic bool `json:"chronic"`
|
||||
TopUp *TopUpInfo `json:"top_up,omitempty"`
|
||||
}
|
||||
|
||||
// TopUpInfo represents top-up information
|
||||
type TopUpInfo struct {
|
||||
Eligible bool `json:"eligible"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
|
||||
// GrouperResponse represents grouper API response
|
||||
type GrouperResponse struct {
|
||||
models.BaseResponse
|
||||
Data *GrouperResult `json:"data,omitempty"`
|
||||
}
|
||||
87
internal/models/qris/qris.go
Normal file
87
internal/models/qris/qris.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package qris
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Qris represents the data structure for the qris table
|
||||
// with proper null handling and optimized JSON marshaling
|
||||
type Qris struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
|
||||
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
|
||||
UpdatedAt sql.NullTime `json:"updated_at,omitempty" db:"updated_at"`
|
||||
DisplayName sql.NullString `json:"display_name,omitempty" db:"display_name"`
|
||||
DisplayAmount float64 `json:"display_amount" db:"display_amount"`
|
||||
QrValue string `json:"qrvalue" db:"qrvalue"`
|
||||
IP string `json:"ip" db:"ip"`
|
||||
}
|
||||
|
||||
// Custom JSON marshaling untuk Qris agar NULL values tidak muncul di response
|
||||
func (r Qris) MarshalJSON() ([]byte, error) {
|
||||
type Alias Qris
|
||||
aux := &struct {
|
||||
Sort *int `json:"sort,omitempty"`
|
||||
UserCreated *string `json:"user_created,omitempty"`
|
||||
UserUpdated *string `json:"user_updated,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
DisplayName *string `json:"display_name,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Sort.Valid {
|
||||
sort := int(r.Sort.Int32)
|
||||
aux.Sort = &sort
|
||||
}
|
||||
if r.UserCreated.Valid {
|
||||
aux.UserCreated = &r.UserCreated.String
|
||||
}
|
||||
if r.UserUpdated.Valid {
|
||||
aux.UserUpdated = &r.UserUpdated.String
|
||||
}
|
||||
if r.UpdatedAt.Valid {
|
||||
aux.UpdatedAt = &r.UpdatedAt.Time
|
||||
}
|
||||
if r.DisplayName.Valid {
|
||||
aux.DisplayName = &r.DisplayName.String
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper methods untuk mendapatkan nilai yang aman
|
||||
func (r *Qris) GetName() string {
|
||||
if r.DisplayName.Valid {
|
||||
return r.DisplayName.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response struct untuk GET by ID
|
||||
type QrisGetByIDResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Qris `json:"data"`
|
||||
}
|
||||
|
||||
// Enhanced GET response dengan pagination dan aggregation
|
||||
type QrisGetResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []Qris `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
Summary *models.AggregateData `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// Filter struct untuk query parameters
|
||||
type QrisFilter struct {
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package peserta
|
||||
|
||||
import "api-service/internal/models"
|
||||
|
||||
// === PESERTA MODELS ===
|
||||
|
||||
// PesertaRequest represents peserta lookup request
|
||||
type PesertaRequest struct {
|
||||
models.BaseRequest
|
||||
NoKartu string `json:"nokartu" validate:"required,min=13,max=13"`
|
||||
NIK string `json:"nik,omitempty" validate:"omitempty,min=16,max=16"`
|
||||
TanggalSEP string `json:"tglsep" validate:"required" example:"2024-01-15"`
|
||||
NoTelepon string `json:"notelp,omitempty" validate:"omitempty,max=15"`
|
||||
}
|
||||
|
||||
// PesertaData represents peserta information from BPJS
|
||||
type PesertaData struct {
|
||||
NoKartu string `json:"noKartu"`
|
||||
NIK string `json:"nik"`
|
||||
Nama string `json:"nama"`
|
||||
Pisa string `json:"pisa"`
|
||||
Sex string `json:"sex"`
|
||||
TanggalLahir string `json:"tglLahir"`
|
||||
TglCetakKartu string `json:"tglCetakKartu"`
|
||||
TglTAT string `json:"tglTAT"`
|
||||
TglTMT string `json:"tglTMT"`
|
||||
StatusPeserta struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
} `json:"statusPeserta"`
|
||||
ProvUmum struct {
|
||||
KdProvider string `json:"kdProvider"`
|
||||
NmProvider string `json:"nmProvider"`
|
||||
} `json:"provUmum"`
|
||||
JenisPeserta struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
} `json:"jenisPeserta"`
|
||||
HakKelas struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
} `json:"hakKelas"`
|
||||
Umur struct {
|
||||
UmurSekarang string `json:"umurSekarang"`
|
||||
UmurSaatPelayanan string `json:"umurSaatPelayanan"`
|
||||
} `json:"umur"`
|
||||
Informasi struct {
|
||||
Dinsos interface{} `json:"dinsos"`
|
||||
ProlanisPRB string `json:"prolanisPRB"`
|
||||
NoSKTM interface{} `json:"noSKTM"`
|
||||
ESEP interface{} `json:"eSEP"`
|
||||
} `json:"informasi"`
|
||||
Cob struct {
|
||||
NoAsuransi interface{} `json:"noAsuransi"`
|
||||
NmAsuransi interface{} `json:"nmAsuransi"`
|
||||
TglTMT interface{} `json:"tglTMT"`
|
||||
TglTAT interface{} `json:"tglTAT"`
|
||||
} `json:"cob"`
|
||||
MR struct {
|
||||
NoMR string `json:"noMR"`
|
||||
NoTelepon string `json:"noTelepon"`
|
||||
} `json:"mr,omitempty"`
|
||||
}
|
||||
|
||||
// PesertaResponse represents peserta API response
|
||||
type PesertaResponse struct {
|
||||
models.BaseResponse
|
||||
Data *PesertaData `json:"data,omitempty"`
|
||||
MetaData interface{} `json:"metaData,omitempty"`
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package rujukan
|
||||
|
||||
import "api-service/internal/models"
|
||||
|
||||
// === RUJUKAN MODELS ===
|
||||
|
||||
// RujukanRequest represents rujukan lookup request
|
||||
type RujukanRequest struct {
|
||||
models.BaseRequest
|
||||
NoRujukan string `json:"noRujukan" validate:"required"`
|
||||
NoKartu string `json:"noKartu,omitempty"`
|
||||
}
|
||||
|
||||
// RujukanData represents rujukan information
|
||||
type RujukanData struct {
|
||||
Diagnosa DiagnosaData `json:"diagnosa"`
|
||||
Keluhan string `json:"keluhan"`
|
||||
NoKunjungan string `json:"noKunjungan"`
|
||||
Pelayanan PelayananData `json:"pelayanan"`
|
||||
Peserta PesertaData `json:"peserta"`
|
||||
PoliRujukan PoliRujukanData `json:"poliRujukan"`
|
||||
ProvPerujuk ProvPerujukData `json:"provPerujuk"`
|
||||
TglKunjungan string `json:"tglKunjungan"`
|
||||
}
|
||||
|
||||
type DiagnosaData struct {
|
||||
Kode string `json:"kode"`
|
||||
Nama string `json:"nama"`
|
||||
}
|
||||
|
||||
type PelayananData struct {
|
||||
Kode string `json:"kode"`
|
||||
Nama string `json:"nama"`
|
||||
}
|
||||
|
||||
type PoliRujukanData struct {
|
||||
Kode string `json:"kode"`
|
||||
Nama string `json:"nama"`
|
||||
}
|
||||
|
||||
type ProvPerujukData struct {
|
||||
Kode string `json:"kode"`
|
||||
Nama string `json:"nama"`
|
||||
}
|
||||
|
||||
type PesertaData struct {
|
||||
NoKartu string `json:"noKartu"`
|
||||
NIK string `json:"nik"`
|
||||
Nama string `json:"nama"`
|
||||
Pisa string `json:"pisa"`
|
||||
Sex string `json:"sex"`
|
||||
TanggalLahir string `json:"tglLahir"`
|
||||
TglCetakKartu string `json:"tglCetakKartu"`
|
||||
TglTAT string `json:"tglTAT"`
|
||||
TglTMT string `json:"tglTMT"`
|
||||
StatusPeserta struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
} `json:"statusPeserta"`
|
||||
ProvUmum struct {
|
||||
KdProvider string `json:"kdProvider"`
|
||||
NmProvider string `json:"nmProvider"`
|
||||
} `json:"provUmum"`
|
||||
JenisPeserta struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
} `json:"jenisPeserta"`
|
||||
HakKelas struct {
|
||||
Kode string `json:"kode"`
|
||||
Keterangan string `json:"keterangan"`
|
||||
} `json:"hakKelas"`
|
||||
Umur struct {
|
||||
UmurSekarang string `json:"umurSekarang"`
|
||||
UmurSaatPelayanan string `json:"umurSaatPelayanan"`
|
||||
} `json:"umur"`
|
||||
Informasi struct {
|
||||
Dinsos interface{} `json:"dinsos"`
|
||||
ProlanisPRB interface{} `json:"prolanisPRB"`
|
||||
NoSKTM interface{} `json:"noSKTM"`
|
||||
} `json:"informasi"`
|
||||
Cob struct {
|
||||
NoAsuransi interface{} `json:"noAsuransi"`
|
||||
NmAsuransi interface{} `json:"nmAsuransi"`
|
||||
TglTMT interface{} `json:"tglTMT"`
|
||||
TglTAT interface{} `json:"tglTAT"`
|
||||
} `json:"cob"`
|
||||
MR struct {
|
||||
NoMR string `json:"noMR"`
|
||||
NoTelepon interface{} `json:"noTelepon"`
|
||||
} `json:"mr"`
|
||||
}
|
||||
|
||||
// RujukanResponse represents rujukan API response
|
||||
type RujukanResponse struct {
|
||||
models.BaseResponse
|
||||
Data *RujukanData `json:"data,omitempty"`
|
||||
List []RujukanData `json:"list,omitempty"`
|
||||
MetaData interface{} `json:"metaData,omitempty"`
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package sep
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"api-service/internal/models/vclaim/peserta"
|
||||
)
|
||||
|
||||
// === SEP (Surat Eligibilitas Peserta) MODELS ===
|
||||
|
||||
// SEPRequest represents SEP creation/update request
|
||||
type SepRequest struct {
|
||||
models.BaseRequest
|
||||
NoKartu string `json:"noKartu" validate:"required"`
|
||||
TglSep string `json:"tglSep" validate:"required"`
|
||||
PPKPelayanan string `json:"ppkPelayanan" validate:"required"`
|
||||
JnsPelayanan string `json:"jnsPelayanan" validate:"required,oneof=1 2"`
|
||||
KlsRawat string `json:"klsRawat" validate:"required,oneof=1 2 3"`
|
||||
NoMR string `json:"noMR" validate:"required"`
|
||||
Rujukan *SepRujukan `json:"rujukan"`
|
||||
Catatan string `json:"catatan,omitempty"`
|
||||
Diagnosa string `json:"diagnosa" validate:"required"`
|
||||
PoliTujuan string `json:"poli" validate:"required"`
|
||||
ExternalUser string `json:"user" validate:"required"`
|
||||
NoTelp string `json:"noTelp,omitempty"`
|
||||
}
|
||||
|
||||
// SEPRujukan represents rujukan information in SEP
|
||||
type SepRujukan struct {
|
||||
AsalRujukan string `json:"asalRujukan" validate:"required,oneof=1 2"`
|
||||
TglRujukan string `json:"tglRujukan" validate:"required"`
|
||||
NoRujukan string `json:"noRujukan" validate:"required"`
|
||||
PPKRujukan string `json:"ppkRujukan" validate:"required"`
|
||||
}
|
||||
|
||||
// SEPData represents SEP response data
|
||||
type SepData struct {
|
||||
NoSep string `json:"noSep"`
|
||||
TglSep string `json:"tglSep"`
|
||||
JnsPelayanan string `json:"jnsPelayanan"`
|
||||
PoliTujuan string `json:"poli"`
|
||||
KlsRawat string `json:"klsRawat"`
|
||||
NoMR string `json:"noMR"`
|
||||
Rujukan SepRujukan `json:"rujukan"`
|
||||
Catatan string `json:"catatan"`
|
||||
Diagnosa string `json:"diagnosa"`
|
||||
Peserta peserta.PesertaData `json:"peserta"`
|
||||
Informasi struct {
|
||||
NoSKDP string `json:"noSKDP,omitempty"`
|
||||
DPJPLayan string `json:"dpjpLayan"`
|
||||
NoTelepon string `json:"noTelp"`
|
||||
SubSpesialis string `json:"subSpesialis,omitempty"`
|
||||
} `json:"informasi"`
|
||||
}
|
||||
|
||||
// SEPResponse represents SEP API response
|
||||
type SepResponse struct {
|
||||
models.BaseResponse
|
||||
Data *SepData `json:"data,omitempty"`
|
||||
}
|
||||
@@ -5,10 +5,8 @@ import (
|
||||
"api-service/internal/database"
|
||||
authHandlers "api-service/internal/handlers/auth"
|
||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||
qrisQrisHandlers "api-service/internal/handlers/qris"
|
||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||
"api-service/internal/handlers/vclaim/peserta"
|
||||
"api-service/internal/handlers/vclaim/rujukan"
|
||||
"api-service/internal/handlers/vclaim/sep"
|
||||
"api-service/internal/middleware"
|
||||
services "api-service/internal/services/auth"
|
||||
"api-service/pkg/logger"
|
||||
@@ -69,37 +67,14 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
|
||||
// ============= PUBLISHED ROUTES ===============================================
|
||||
|
||||
// Rujukan routes
|
||||
rujukanHandler := rujukan.NewVClaimHandler(rujukan.VClaimHandlerConfig{
|
||||
BpjsConfig: cfg.Bpjs,
|
||||
Logger: *logger.Default(),
|
||||
Validator: nil,
|
||||
})
|
||||
rujukanGroup := v1.Group("/rujukan")
|
||||
rujukanGroup.GET("/nokartu/:nokartu", rujukanHandler.GetRujukanBynokartu)
|
||||
rujukanGroup.GET("/norujukan/:norujukan", rujukanHandler.GetRujukanBynorujukan)
|
||||
|
||||
// Peserta routes
|
||||
pesertaHandler := peserta.NewVClaimHandler(peserta.VClaimHandlerConfig{
|
||||
BpjsConfig: cfg.Bpjs,
|
||||
Logger: *logger.Default(),
|
||||
Validator: nil,
|
||||
})
|
||||
pesertaGroup := v1.Group("/peserta")
|
||||
pesertaGroup.GET("/nokartu/:nokartu/tglSEP/:tglsep", pesertaHandler.GetPesertaBynokartu)
|
||||
pesertaGroup.GET("/nik/:nik/tglSEP/:tglsep", pesertaHandler.GetPesertaBynik)
|
||||
|
||||
// Sep routes
|
||||
sepHandler := sep.NewVClaimHandler(sep.VClaimHandlerConfig{
|
||||
BpjsConfig: cfg.Bpjs,
|
||||
Logger: *logger.Default(),
|
||||
Validator: nil,
|
||||
})
|
||||
sepGroup := v1.Group("/sep")
|
||||
sepGroup.GET("/sep/:nosep", sepHandler.GetSepSep)
|
||||
sepGroup.POST("/sep", sepHandler.CreateSepSep)
|
||||
sepGroup.PUT("/sep/:nosep", sepHandler.UpdateSepSep)
|
||||
sepGroup.DELETE("/sep/:nosep", sepHandler.DeleteSepSep)
|
||||
// Qris endpoints
|
||||
qrisQrisHandler := qrisQrisHandlers.NewQrisHandler()
|
||||
qrisQrisGroup := v1.Group("/qris")
|
||||
{
|
||||
qrisQrisGroup.GET("", qrisQrisHandler.GetQris)
|
||||
qrisQrisGroup.GET("/:id", qrisQrisHandler.GetQrisByID)
|
||||
qrisQrisGroup.GET("/stats", qrisQrisHandler.GetQrisStats)
|
||||
}
|
||||
|
||||
// // Retribusi endpoints
|
||||
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
helper "api-service/internal/helpers/bpjs"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"unicode/utf16"
|
||||
|
||||
lzstring "github.com/daku10/go-lz-string"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ResponseVclaim decrypts the encrypted response from VClaim API
|
||||
func ResponseVclaim(encrypted string, key string) (string, error) {
|
||||
log.Println("ResponseVclaim: Starting decryption process")
|
||||
log.Printf("ResponseVclaim: Encrypted string length: %d", len(encrypted))
|
||||
|
||||
// Pad the base64 string if needed
|
||||
if len(encrypted)%4 != 0 {
|
||||
padding := (4 - len(encrypted)%4) % 4
|
||||
for i := 0; i < padding; i++ {
|
||||
encrypted += "="
|
||||
}
|
||||
}
|
||||
|
||||
// Decode base64
|
||||
cipherText, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
log.Printf("ResponseVclaim: Failed to decode base64: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherText) < aes.BlockSize {
|
||||
return "", errors.New("cipherText too short")
|
||||
}
|
||||
|
||||
// Create AES cipher
|
||||
hash := sha256.Sum256([]byte(key))
|
||||
block, err := aes.NewCipher(hash[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Try both IV methods
|
||||
// Method 1: IV from hash (current approach)
|
||||
if result, err := tryDecryptWithHashIV(cipherText, block, hash[:aes.BlockSize]); err == nil {
|
||||
log.Printf("ResponseVclaim: Success with hash IV method")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 2: IV from cipherText (standard approach)
|
||||
if result, err := tryDecryptWithCipherIV(cipherText, block); err == nil {
|
||||
log.Printf("ResponseVclaim: Success with cipher IV method")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return "", errors.New("all decryption methods failed")
|
||||
}
|
||||
|
||||
func tryDecryptWithHashIV(cipherText []byte, block cipher.Block, iv []byte) (string, error) {
|
||||
if len(cipherText)%aes.BlockSize != 0 {
|
||||
return "", errors.New("cipherText is not a multiple of the block size")
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
decrypted := make([]byte, len(cipherText))
|
||||
mode.CryptBlocks(decrypted, cipherText)
|
||||
|
||||
// Remove PKCS7 padding
|
||||
decrypted = helper.RemovePKCS7Padding(decrypted)
|
||||
log.Printf("tryDecryptWithHashIV: Decryption completed, length: %d", len(decrypted))
|
||||
|
||||
return tryAllDecompressionMethods(decrypted)
|
||||
}
|
||||
|
||||
func tryDecryptWithCipherIV(cipherText []byte, block cipher.Block) (string, error) {
|
||||
if len(cipherText) < aes.BlockSize {
|
||||
return "", errors.New("cipherText too short for IV extraction")
|
||||
}
|
||||
|
||||
// Extract IV from first block
|
||||
iv := cipherText[:aes.BlockSize]
|
||||
cipherData := cipherText[aes.BlockSize:]
|
||||
|
||||
if len(cipherData)%aes.BlockSize != 0 {
|
||||
return "", errors.New("cipher data is not a multiple of the block size")
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
decrypted := make([]byte, len(cipherData))
|
||||
mode.CryptBlocks(decrypted, cipherData)
|
||||
|
||||
// Remove PKCS7 padding
|
||||
decrypted = helper.RemovePKCS7Padding(decrypted)
|
||||
log.Printf("tryDecryptWithCipherIV: Decryption completed, length: %d", len(decrypted))
|
||||
|
||||
return tryAllDecompressionMethods(decrypted)
|
||||
}
|
||||
|
||||
func tryAllDecompressionMethods(data []byte) (string, error) {
|
||||
log.Printf("tryAllDecompressionMethods: Attempting decompression, data length: %d", len(data))
|
||||
|
||||
// Method 1: Check if it's already valid JSON
|
||||
if isValidJSON(data) {
|
||||
log.Println("tryAllDecompressionMethods: Data is valid JSON, returning as-is")
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// Method 2: Try gzip decompression
|
||||
if result, err := tryGzipDecompression(data); err == nil && len(result) > 0 {
|
||||
log.Println("tryAllDecompressionMethods: Gzip decompression successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 3: Try LZ-string decompression methods
|
||||
if result, err := tryLZStringMethods(data); err == nil && len(result) > 0 {
|
||||
log.Println("tryAllDecompressionMethods: LZ-string decompression successful")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 4: Return as plain text
|
||||
result := string(data)
|
||||
if len(result) > 0 {
|
||||
log.Printf("tryAllDecompressionMethods: Using decrypted data as plain text, length: %d", len(result))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return "", errors.New("all decompression methods failed")
|
||||
}
|
||||
|
||||
func isValidJSON(data []byte) bool {
|
||||
if len(data) == 0 {
|
||||
return false
|
||||
}
|
||||
firstChar := data[0]
|
||||
return firstChar == '{' || firstChar == '['
|
||||
}
|
||||
|
||||
func tryGzipDecompression(data []byte) (string, error) {
|
||||
reader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(decompressed), nil
|
||||
}
|
||||
|
||||
func tryLZStringMethods(data []byte) (string, error) {
|
||||
dataStr := string(data)
|
||||
|
||||
// Method 1: DecompressFromEncodedURIComponent
|
||||
if result, err := lzstring.DecompressFromEncodedURIComponent(dataStr); err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 2: DecompressFromBase64
|
||||
if result, err := lzstring.DecompressFromBase64(dataStr); err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Method 3: DecompressFromUTF16 (with proper conversion)
|
||||
if utf16Data, err := stringToUTF16(dataStr); err == nil {
|
||||
if result, err := lzstring.DecompressFromUTF16(utf16Data); err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Try with base64 decoding first
|
||||
if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
|
||||
if result, err := lzstring.DecompressFromEncodedURIComponent(string(decoded)); err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("all LZ-string methods failed")
|
||||
}
|
||||
|
||||
// stringToUTF16 converts string to []uint16 for UTF16 decompression
|
||||
func stringToUTF16(s string) ([]uint16, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, errors.New("empty string")
|
||||
}
|
||||
|
||||
// Convert string to runes first
|
||||
runes := []rune(s)
|
||||
|
||||
// Convert runes to UTF16
|
||||
utf16Data := utf16.Encode(runes)
|
||||
|
||||
return utf16Data, nil
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/models/vclaim/peserta"
|
||||
|
||||
"github.com/mashingan/smapping"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// VClaimService interface for VClaim operations
|
||||
type VClaimService interface {
|
||||
Get(ctx context.Context, endpoint string, result interface{}) error
|
||||
Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
|
||||
Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
|
||||
Patch(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
|
||||
Delete(ctx context.Context, endpoint string, result interface{}) error
|
||||
GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error)
|
||||
PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error)
|
||||
PutRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error)
|
||||
PatchRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error)
|
||||
DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error)
|
||||
}
|
||||
|
||||
// Service struct for VClaim service
|
||||
type Service struct {
|
||||
config config.BpjsConfig
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Response structures
|
||||
type ResponMentahDTOVclaim struct {
|
||||
MetaData struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"metaData"`
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
type ResponDTOVclaim struct {
|
||||
MetaData struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"metaData"`
|
||||
Response interface{} `json:"response"`
|
||||
}
|
||||
|
||||
// NewService creates a new VClaim service instance
|
||||
func NewService(cfg config.BpjsConfig) VClaimService {
|
||||
log.Info().
|
||||
Str("base_url", cfg.BaseURL).
|
||||
Dur("timeout", cfg.Timeout).
|
||||
Msg("Creating new VClaim service instance")
|
||||
|
||||
service := &Service{
|
||||
config: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
},
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
// NewServiceFromConfig creates service from main config
|
||||
func NewServiceFromConfig(cfg *config.Config) VClaimService {
|
||||
return NewService(cfg.Bpjs)
|
||||
}
|
||||
|
||||
// NewServiceFromInterface creates service from interface (for backward compatibility)
|
||||
func NewServiceFromInterface(cfg interface{}) (VClaimService, error) {
|
||||
var bpjsConfig config.BpjsConfig
|
||||
|
||||
// Try to map from interface
|
||||
err := smapping.FillStruct(&bpjsConfig, smapping.MapFields(&cfg))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map config: %w", err)
|
||||
}
|
||||
|
||||
if bpjsConfig.Timeout == 0 {
|
||||
bpjsConfig.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return NewService(bpjsConfig), nil
|
||||
}
|
||||
|
||||
// SetHTTPClient allows custom http client configuration
|
||||
func (s *Service) SetHTTPClient(client *http.Client) {
|
||||
s.httpClient = client
|
||||
}
|
||||
|
||||
// prepareRequest prepares HTTP request with required headers
|
||||
func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
|
||||
fullURL := s.config.BaseURL + endpoint
|
||||
|
||||
log.Info().
|
||||
Str("method", method).
|
||||
Str("endpoint", endpoint).
|
||||
Str("full_url", fullURL).
|
||||
Msg("Preparing HTTP request")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("method", method).
|
||||
Str("endpoint", endpoint).
|
||||
Msg("Failed to create HTTP request")
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers using the SetHeader method
|
||||
consID, _, userKey, tstamp, xSignature := s.config.SetHeader()
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-cons-id", consID)
|
||||
req.Header.Set("X-timestamp", tstamp)
|
||||
req.Header.Set("X-signature", xSignature)
|
||||
req.Header.Set("user_key", userKey)
|
||||
|
||||
log.Debug().
|
||||
Str("method", method).
|
||||
Str("endpoint", endpoint).
|
||||
Str("x_cons_id", consID).
|
||||
Str("x_timestamp", tstamp).
|
||||
Str("user_key", userKey).
|
||||
Msg("Request headers set")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// processResponse processes response from VClaim API
|
||||
func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("HTTP error: %d - %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse raw response
|
||||
var respMentah ResponMentahDTOVclaim
|
||||
if err := json.Unmarshal(body, &respMentah); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal raw response: %w", err)
|
||||
}
|
||||
|
||||
// Create final response
|
||||
finalResp := &ResponDTOVclaim{
|
||||
MetaData: respMentah.MetaData,
|
||||
}
|
||||
|
||||
// Check if response needs decryption
|
||||
if respMentah.Response == "" {
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Try to parse as JSON first (unencrypted response)
|
||||
var tempResp interface{}
|
||||
if json.Unmarshal([]byte(respMentah.Response), &tempResp) == nil {
|
||||
finalResp.Response = tempResp
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Decrypt response
|
||||
consID, secretKey, _, tstamp, _ := s.config.SetHeader()
|
||||
decryptionKey := consID + secretKey + tstamp
|
||||
|
||||
log.Debug().
|
||||
Str("consID", consID).
|
||||
Str("tstamp", tstamp).
|
||||
Int("key_length", len(decryptionKey)).
|
||||
Msg("Decryption key components")
|
||||
|
||||
respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decrypt response")
|
||||
return nil, fmt.Errorf("failed to decrypt response: %w", err)
|
||||
}
|
||||
|
||||
// Try to unmarshal decrypted response as JSON
|
||||
if respDecrypt != "" {
|
||||
if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil {
|
||||
// If JSON unmarshal fails, store as string
|
||||
log.Warn().Err(err).Msg("Failed to unmarshal decrypted response, storing as string")
|
||||
finalResp.Response = respDecrypt
|
||||
}
|
||||
}
|
||||
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Get performs HTTP GET request
|
||||
func (s *Service) Get(ctx context.Context, endpoint string, result interface{}) error {
|
||||
resp, err := s.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Post performs HTTP POST request
|
||||
func (s *Service) Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
|
||||
resp, err := s.PostRawResponse(ctx, endpoint, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Put performs HTTP PUT request
|
||||
func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute PUT request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Delete performs HTTP DELETE request
|
||||
func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error {
|
||||
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute DELETE request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Patch performs HTTP PATCH request
|
||||
func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute PATCH request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// GetRawResponse returns raw response without mapping
|
||||
func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
||||
req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute GET request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// PostRawResponse returns raw response without mapping
|
||||
func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error) {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute POST request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// PatchRawResponse returns raw response without mapping
|
||||
func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error) {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPatch, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute PATCH request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// PutRawResponse returns raw response without mapping
|
||||
func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error) {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute PUT request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// DeleteRawResponse returns raw response without mapping
|
||||
func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
||||
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute DELETE request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// mapToResult maps the final response to the result interface
|
||||
func mapToResult(resp *ResponDTOVclaim, result interface{}) error {
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal final response: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBytes, result); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal to result: %w", err)
|
||||
}
|
||||
|
||||
// Handle BPJS peserta response structure
|
||||
if pesertaResp, ok := result.(*peserta.PesertaResponse); ok {
|
||||
if resp.Response != nil {
|
||||
if responseMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
if pesertaMap, ok := responseMap["peserta"]; ok {
|
||||
pesertaBytes, _ := json.Marshal(pesertaMap)
|
||||
var pd peserta.PesertaData
|
||||
json.Unmarshal(pesertaBytes, &pd)
|
||||
pesertaResp.Data = &pd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backward compatibility functions
|
||||
func GetRequest(endpoint string, cfg interface{}) interface{} {
|
||||
service, err := NewServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create service: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get response: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{} {
|
||||
service, err := NewServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create service: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.PostRawResponse(ctx, endpoint, data)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to post response: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
@@ -1,676 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/mashingan/smapping"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// SatuSehatService interface for SATUSEHAT operations
|
||||
type SatuSehatService interface {
|
||||
// Standard HTTP methods
|
||||
Get(ctx context.Context, endpoint string, result interface{}) error
|
||||
Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
|
||||
Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
|
||||
Delete(ctx context.Context, endpoint string, result interface{}) error
|
||||
|
||||
// Raw response methods
|
||||
GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, error)
|
||||
PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, error)
|
||||
|
||||
// FHIR specific methods
|
||||
PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error)
|
||||
GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
|
||||
GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
|
||||
GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error)
|
||||
|
||||
// Token management
|
||||
RefreshToken(ctx context.Context) error
|
||||
IsTokenValid() bool
|
||||
GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error)
|
||||
}
|
||||
|
||||
// SatuSehatService struct for SATUSEHAT service
|
||||
type SatuSehatServiceStruct struct {
|
||||
config config.SatuSehatConfig
|
||||
httpClient *http.Client
|
||||
token TokenDetail
|
||||
tokenMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Token detail structure
|
||||
type TokenDetail struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
OrganizationName string `json:"organization_name"`
|
||||
DeveloperEmail string `json:"developer.email"`
|
||||
ClientID string `json:"client_id"`
|
||||
ApplicationName string `json:"application_name"`
|
||||
Status string `json:"status"`
|
||||
ExpiryTime time.Time `json:"-"`
|
||||
}
|
||||
|
||||
// Response structures
|
||||
type SatuSehatResponMentahDTO struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type SatuSehatResponDTO struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Error *ErrorInfo `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorInfo struct {
|
||||
Code string `json:"code"`
|
||||
Details string `json:"details"`
|
||||
}
|
||||
|
||||
// Token methods
|
||||
func (t *TokenDetail) IsExpired() bool {
|
||||
if t.ExpiryTime.IsZero() {
|
||||
return true
|
||||
}
|
||||
return time.Now().UTC().After(t.ExpiryTime.Add(-5 * time.Minute))
|
||||
}
|
||||
|
||||
func (t *TokenDetail) SetExpired() {
|
||||
t.ExpiryTime = time.Time{}
|
||||
}
|
||||
|
||||
// NewSatuSehatService creates a new SATUSEHAT service instance
|
||||
func NewSatuSehatService(cfg config.SatuSehatConfig) SatuSehatService {
|
||||
service := &SatuSehatServiceStruct{
|
||||
config: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
},
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// NewSatuSehatServiceFromConfig creates service from main config
|
||||
func NewSatuSehatServiceFromConfig(cfg *config.Config) SatuSehatService {
|
||||
return NewSatuSehatService(cfg.SatuSehat)
|
||||
}
|
||||
|
||||
// NewSatuSehatServiceFromInterface creates service from interface (for backward compatibility)
|
||||
func NewSatuSehatServiceFromInterface(cfg interface{}) (SatuSehatService, error) {
|
||||
var satusehatConfig config.SatuSehatConfig
|
||||
|
||||
// Try to map from interface
|
||||
err := smapping.FillStruct(&satusehatConfig, smapping.MapFields(&cfg))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map config: %w", err)
|
||||
}
|
||||
|
||||
if satusehatConfig.Timeout == 0 {
|
||||
satusehatConfig.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return NewSatuSehatService(satusehatConfig), nil
|
||||
}
|
||||
|
||||
// SetHTTPClient allows custom http client configuration
|
||||
func (s *SatuSehatServiceStruct) SetHTTPClient(client *http.Client) {
|
||||
s.httpClient = client
|
||||
}
|
||||
|
||||
// RefreshToken obtains new access token
|
||||
func (s *SatuSehatServiceStruct) RefreshToken(ctx context.Context) error {
|
||||
s.tokenMutex.Lock()
|
||||
defer s.tokenMutex.Unlock()
|
||||
|
||||
// Double-check pattern
|
||||
if !s.token.IsExpired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
|
||||
tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
|
||||
|
||||
formData := fmt.Sprintf("client_id=%s&client_secret=%s", s.config.ClientID, s.config.ClientSecret)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(formData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute token request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read token response: %w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
// Log the error response for debugging
|
||||
fmt.Printf("DEBUG: Token request failed with status %d: %s\n", res.StatusCode, string(body))
|
||||
return fmt.Errorf("token request failed with status %d: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Debug: log the raw response for troubleshooting
|
||||
fmt.Printf("DEBUG: SATUSEHAT token response - Status: %d, Body: %s\n", res.StatusCode, string(body))
|
||||
fmt.Printf("DEBUG: Request URL: %s\n", tokenURL)
|
||||
fmt.Printf("DEBUG: Request Headers: %+v\n", req.Header)
|
||||
|
||||
return s.parseTokenResponse(body)
|
||||
}
|
||||
|
||||
// parseTokenResponse parses token response from SATUSEHAT
|
||||
func (s *SatuSehatServiceStruct) parseTokenResponse(body []byte) error {
|
||||
// Debug: log the raw response for detailed analysis
|
||||
fmt.Printf("DEBUG: Raw token response body: %s\n", string(body))
|
||||
|
||||
result := gjson.ParseBytes(body)
|
||||
|
||||
// Check if we have a valid access token
|
||||
accessToken := result.Get("access_token").String()
|
||||
if accessToken == "" {
|
||||
return fmt.Errorf("no access token found in response: %s", string(body))
|
||||
}
|
||||
|
||||
issuedAt := result.Get("issued_at").Int()
|
||||
expiresIn := result.Get("expires_in").Int()
|
||||
|
||||
// Handle timestamp conversion (issued_at could be in milliseconds or seconds)
|
||||
var expiryTime time.Time
|
||||
if issuedAt > 1000000000000 { // If timestamp is in milliseconds
|
||||
expiryTime = time.Unix(issuedAt/1000, 0).Add(time.Duration(expiresIn) * time.Second)
|
||||
} else if issuedAt > 0 { // If timestamp is in seconds
|
||||
expiryTime = time.Unix(issuedAt, 0).Add(time.Duration(expiresIn) * time.Second)
|
||||
} else {
|
||||
// If no issued_at, use current time + expires_in
|
||||
expiryTime = time.Now().UTC().Add(time.Duration(expiresIn) * time.Second)
|
||||
}
|
||||
|
||||
s.token = TokenDetail{
|
||||
AccessToken: accessToken,
|
||||
TokenType: result.Get("token_type").String(),
|
||||
ExpiresIn: expiresIn,
|
||||
IssuedAt: issuedAt,
|
||||
OrganizationName: result.Get("organization_name").String(),
|
||||
DeveloperEmail: result.Get("developer\\.email").String(),
|
||||
ClientID: result.Get("client_id").String(),
|
||||
ApplicationName: result.Get("application_name").String(),
|
||||
Status: result.Get("status").String(),
|
||||
ExpiryTime: expiryTime,
|
||||
}
|
||||
|
||||
logger.Info("SATUSEHAT token refreshed successfully", map[string]interface{}{
|
||||
"expires_at": s.token.ExpiryTime,
|
||||
"organization": s.token.OrganizationName,
|
||||
"token_type": s.token.TokenType,
|
||||
"client_id": s.token.ClientID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTokenValid checks if current token is valid
|
||||
func (s *SatuSehatServiceStruct) IsTokenValid() bool {
|
||||
s.tokenMutex.RLock()
|
||||
defer s.tokenMutex.RUnlock()
|
||||
return !s.token.IsExpired()
|
||||
}
|
||||
|
||||
// ensureValidToken ensures we have a valid token
|
||||
func (s *SatuSehatServiceStruct) ensureValidToken(ctx context.Context) error {
|
||||
s.tokenMutex.RLock()
|
||||
needsRefresh := s.token.IsExpired()
|
||||
s.tokenMutex.RUnlock()
|
||||
|
||||
if needsRefresh {
|
||||
return s.RefreshToken(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareRequest prepares HTTP request with required headers
|
||||
func (s *SatuSehatServiceStruct) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
|
||||
// Ensure valid token
|
||||
if err := s.ensureValidToken(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure valid token: %w", err)
|
||||
}
|
||||
|
||||
fullURL := s.config.BaseURL + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
s.tokenMutex.RLock()
|
||||
token := s.token.AccessToken
|
||||
s.tokenMutex.RUnlock()
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// processResponse processes response from SATUSEHAT API
|
||||
func (s *SatuSehatServiceStruct) processResponse(res *http.Response) (*SatuSehatResponDTO, error) {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
// Create response
|
||||
resp := &SatuSehatResponDTO{
|
||||
StatusCode: res.StatusCode,
|
||||
Success: res.StatusCode >= 200 && res.StatusCode < 300,
|
||||
}
|
||||
|
||||
// Handle different status codes
|
||||
switch {
|
||||
case res.StatusCode == 401:
|
||||
s.tokenMutex.Lock()
|
||||
s.token.SetExpired()
|
||||
s.tokenMutex.Unlock()
|
||||
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "UNAUTHORIZED",
|
||||
Details: "Token expired or invalid",
|
||||
}
|
||||
resp.Message = "Unauthorized access"
|
||||
|
||||
case res.StatusCode >= 400 && res.StatusCode < 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "CLIENT_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Client error"
|
||||
|
||||
case res.StatusCode >= 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "SERVER_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Server error"
|
||||
|
||||
default:
|
||||
resp.Message = "Success"
|
||||
}
|
||||
|
||||
// Parse JSON response if successful
|
||||
if resp.Success && len(body) > 0 {
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(body, &jsonData); err != nil {
|
||||
// If JSON unmarshal fails, store as string
|
||||
resp.Data = string(body)
|
||||
} else {
|
||||
resp.Data = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Get performs HTTP GET request
|
||||
func (s *SatuSehatServiceStruct) Get(ctx context.Context, endpoint string, result interface{}) error {
|
||||
resp, err := s.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Post performs HTTP POST request
|
||||
func (s *SatuSehatServiceStruct) Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
|
||||
resp, err := s.PostRawResponse(ctx, endpoint, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Put performs HTTP PUT request
|
||||
func (s *SatuSehatServiceStruct) Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute PUT request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// Delete performs HTTP DELETE request
|
||||
func (s *SatuSehatServiceStruct) Delete(ctx context.Context, endpoint string, result interface{}) error {
|
||||
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute DELETE request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := s.processResponse(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mapToResult(resp, result)
|
||||
}
|
||||
|
||||
// GetRawResponse returns raw response without mapping
|
||||
func (s *SatuSehatServiceStruct) GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, error) {
|
||||
req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute GET request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// PostRawResponse returns raw response without mapping
|
||||
func (s *SatuSehatServiceStruct) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, error) {
|
||||
var buf bytes.Buffer
|
||||
if payload != nil {
|
||||
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute POST request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res)
|
||||
}
|
||||
|
||||
// FHIR-specific methods
|
||||
|
||||
// PostBundle posts FHIR bundle to SATUSEHAT
|
||||
func (s *SatuSehatServiceStruct) PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error) {
|
||||
return s.PostRawResponse(ctx, "", bundle)
|
||||
}
|
||||
|
||||
// GetPatientByNIK retrieves patient by NIK
|
||||
func (s *SatuSehatServiceStruct) GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
|
||||
endpoint := fmt.Sprintf("/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
|
||||
return s.GetRawResponse(ctx, endpoint)
|
||||
}
|
||||
|
||||
// GetPractitionerByNIK retrieves practitioner by NIK
|
||||
func (s *SatuSehatServiceStruct) GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
|
||||
endpoint := fmt.Sprintf("/Practitioner?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
|
||||
return s.GetRawResponse(ctx, endpoint)
|
||||
}
|
||||
|
||||
// GetResourceByID retrieves any FHIR resource by ID
|
||||
func (s *SatuSehatServiceStruct) GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error) {
|
||||
endpoint := fmt.Sprintf("/%s/%s", resourceType, id)
|
||||
return s.GetRawResponse(ctx, endpoint)
|
||||
}
|
||||
|
||||
// GenerateToken generates a new access token with custom client credentials
|
||||
func (s *SatuSehatServiceStruct) GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error) {
|
||||
// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
|
||||
tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
|
||||
|
||||
formData := fmt.Sprintf("client_id=%s&client_secret=%s", clientID, clientSecret)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(formData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create token request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute token request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token response: %w", err)
|
||||
}
|
||||
|
||||
// Process the response using the existing response processor
|
||||
resp := &SatuSehatResponDTO{
|
||||
StatusCode: res.StatusCode,
|
||||
Success: res.StatusCode >= 200 && res.StatusCode < 300,
|
||||
}
|
||||
|
||||
// Handle different status codes
|
||||
switch {
|
||||
case res.StatusCode == 401:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "UNAUTHORIZED",
|
||||
Details: "Invalid client credentials",
|
||||
}
|
||||
resp.Message = "Unauthorized access"
|
||||
|
||||
case res.StatusCode >= 400 && res.StatusCode < 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "CLIENT_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Client error"
|
||||
|
||||
case res.StatusCode >= 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "SERVER_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Server error"
|
||||
|
||||
default:
|
||||
resp.Message = "Success"
|
||||
}
|
||||
|
||||
// Parse JSON response if successful
|
||||
if resp.Success && len(body) > 0 {
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(body, &jsonData); err != nil {
|
||||
// If JSON unmarshal fails, store as string
|
||||
resp.Data = string(body)
|
||||
} else {
|
||||
resp.Data = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// mapToResult maps the final response to the result interface
|
||||
func mapToResult(resp *SatuSehatResponDTO, result interface{}) error {
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal final response: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(respBytes, result); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal to result: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backward compatibility functions
|
||||
|
||||
func SatuSehatGetRequest(endpoint string, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get SATUSEHAT response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func SatuSehatPostRequest(endpoint string, cfg interface{}, data interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.PostRawResponse(ctx, endpoint, data)
|
||||
if err != nil {
|
||||
logger.Error("Failed to post SATUSEHAT response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// FHIR helper functions
|
||||
|
||||
func SatuSehatGetPatient(nik string, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetPatientByNIK(ctx, nik)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get patient", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"nik": nik,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func SatuSehatGetPractitioner(nik string, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetPractitionerByNIK(ctx, nik)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get practitioner", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"nik": nik,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func SatuSehatPostBundle(bundle interface{}, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.PostBundle(ctx, bundle)
|
||||
if err != nil {
|
||||
logger.Error("Failed to post bundle", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user