Initial CPONE middleware implementation

This commit is contained in:
2026-08-18 13:41:38 +07:00
commit 676d930561
17 changed files with 1000 additions and 0 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
APP_HOST=0.0.0.0
APP_PORT=8080
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=primaya
DB_USERNAME=root
DB_PASSWORD=
CPONE_BEARER_TOKEN=replace-with-a-secure-token

18
.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
# Local configuration and secrets
.env
.env.*
!.env.example
# Build outputs
/cpone-middleware
/dist/
/bin/
# Test and coverage outputs
*.test
coverage.out
# Editor and OS files
.idea/
.vscode/
.DS_Store

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# CPONE Middleware Go
Service Go untuk endpoint:
```text
GET /api/cpone/layanan-mapping-harga?periodeTarifID=10&layananID=6413,6414,1309
Authorization: Bearer <your-secure-token>
```
Format response dibuat mengikuti controller PHP `LayananDepartemenController::mappingHarga`.
## Config
Copy `.env.example` menjadi `.env`, lalu isi koneksi MySQL:
```text
APP_HOST=0.0.0.0
APP_PORT=8080
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=primaya
DB_USERNAME=root
DB_PASSWORD=
CPONE_BEARER_TOKEN=replace-with-a-secure-token
```
## Run
```bash
cd cpone-middleware
go mod tidy
go run ./cmd/server
```
## SQLC
File `sqlc.yaml`, `sqlc/schema.sql`, dan `sqlc/query.sql` sudah disiapkan.
Catatan penting: sqlc tidak bisa menerima nama tabel dinamis seperti `harga_layanan_{periodeTarifID}`. Implementasi runtime saat ini tetap memakai `database/sql` untuk menyusun nama tabel setelah `periodeTarifID` divalidasi angka, sama seperti PHP memakai `HargaLayanan::makeTableName()`. Kalau semua periode ingin digenerate oleh sqlc, buat query konkret per tabel periode, misalnya `harga_layanan_10`, `harga_layanan_11`, dan seterusnya.

76
cmd/server/main.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"context"
"database/sql"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
_ "github.com/go-sql-driver/mysql"
"primaya-api/cpone-middleware/internal/config"
apphttp "primaya-api/cpone-middleware/internal/http"
"primaya-api/cpone-middleware/internal/repository"
)
func main() {
cfg, err := config.Load(".env")
if err != nil {
log.Fatalf("load config: %v", err)
}
db, err := sql.Open("mysql", cfg.MySQLDSN())
if err != nil {
log.Fatalf("open mysql: %v", err)
}
defer db.Close()
db.SetMaxOpenConns(cfg.DBMaxOpenConns)
db.SetMaxIdleConns(cfg.DBMaxIdleConns)
db.SetConnMaxLifetime(cfg.DBConnMaxLifetime)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatalf("ping mysql: %v", err)
}
repo := repository.NewMySQLLayananRepository(db)
handler := apphttp.NewHandler(repo)
mux := http.NewServeMux()
mux.Handle("GET /api/cpone/layanan-mapping-harga", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.MappingHarga)))
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"success":true,"message":"ok","data":null}`))
})
server := &http.Server{
Addr: cfg.Addr(),
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
log.Printf("cpone middleware listening on %s", cfg.Addr())
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("shutdown: %v", err)
}
}

7
go.mod Normal file
View File

@@ -0,0 +1,7 @@
module primaya-api/cpone-middleware
go 1.24
require github.com/go-sql-driver/mysql v1.9.3
require filippo.io/edwards25519 v1.1.0 // indirect

4
go.sum Normal file
View File

@@ -0,0 +1,4 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=

131
internal/config/config.go Normal file
View File

@@ -0,0 +1,131 @@
package config
import (
"bufio"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
AppHost string
AppPort string
DBHost string
DBPort string
DBDatabase string
DBUsername string
DBPassword string
BearerToken string
DBMaxOpenConns int
DBMaxIdleConns int
DBConnMaxLifetime time.Duration
}
func Load(path string) (Config, error) {
if err := loadDotEnv(path); err != nil {
return Config{}, err
}
cfg := Config{
AppHost: env("APP_HOST", "0.0.0.0"),
AppPort: env("APP_PORT", "8080"),
DBHost: env("DB_HOST", "127.0.0.1"),
DBPort: env("DB_PORT", "3306"),
DBDatabase: env("DB_DATABASE", ""),
DBUsername: env("DB_USERNAME", "root"),
DBPassword: env("DB_PASSWORD", ""),
BearerToken: env("CPONE_BEARER_TOKEN", ""),
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
DBConnMaxLifetime: time.Duration(envInt("DB_CONN_MAX_LIFETIME_MINUTES", 5)) * time.Minute,
}
if cfg.DBDatabase == "" {
return Config{}, fmt.Errorf("DB_DATABASE wajib diisi")
}
if cfg.BearerToken == "" {
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
}
return cfg, nil
}
func (c Config) Addr() string {
return c.AppHost + ":" + c.AppPort
}
func (c Config) MySQLDSN() string {
values := url.Values{}
values.Set("charset", "utf8mb4")
values.Set("parseTime", "true")
values.Set("loc", "Local")
return fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?%s",
c.DBUsername,
c.DBPassword,
c.DBHost,
c.DBPort,
c.DBDatabase,
values.Encode(),
)
}
func loadDotEnv(path string) error {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
value = strings.Trim(value, `"'`)
if key != "" {
_ = os.Setenv(key, value)
}
}
return scanner.Err()
}
func env(key string, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func envInt(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}

31
internal/db/db.go Normal file
View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

39
internal/db/models.go Normal file
View File

@@ -0,0 +1,39 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"database/sql"
)
type HargaLayanan10 struct {
Hargalayananid int64 `json:"hargalayananid"`
Layanandepartemenid sql.NullInt64 `json:"layanandepartemenid"`
Kelasid sql.NullString `json:"kelasid"`
Na sql.NullString `json:"na"`
Harga sql.NullString `json:"harga"`
Sharetipe sql.NullString `json:"sharetipe"`
Sharedokter sql.NullString `json:"sharedokter"`
Sharers sql.NullString `json:"sharers"`
Shareexternal sql.NullString `json:"shareexternal"`
Sharelain sql.NullString `json:"sharelain"`
}
type Jenislayanan struct {
Jenislayananid int64 `json:"jenislayananid"`
Nama sql.NullString `json:"nama"`
}
type Layanandeparteman struct {
Layanandepartemenid int64 `json:"layanandepartemenid"`
Layananid sql.NullString `json:"layananid"`
Na sql.NullString `json:"na"`
}
type Masterlayanan struct {
Layananid string `json:"layananid"`
Nama sql.NullString `json:"nama"`
Jenislayananid sql.NullInt64 `json:"jenislayananid"`
}

135
internal/db/query.sql.go Normal file
View File

@@ -0,0 +1,135 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: query.sql
package db
import (
"context"
"database/sql"
"strings"
)
const mappingHargaPeriode10 = `-- name: MappingHargaPeriode10 :many
SELECT
tarif.HargaLayananID AS TarifLayananID,
layanan_dept.LayananID,
layanan.Nama AS NamaLayanan,
layanan.JenisLayananID,
jenis.Nama AS NamaJenisLayanan,
'Default' AS Kelas,
'Default' AS NamaKelas,
tarif.Harga,
'Percentage' AS TipeDiskonRJ,
'0.00' AS DiskonRJ,
'Percentage' AS TipeDiskonRI,
'0.00' AS DiskonRI,
tarif.ShareTipe AS TipeShare,
tarif.ShareDokter,
tarif.ShareRS,
tarif.ShareExternal AS ShareExt,
tarif.ShareLain,
'+Layanan' AS AsalTarif
FROM layanandepartemen AS layanan_dept
INNER JOIN harga_layanan_10 AS tarif
ON tarif.LayananDepartemenID = layanan_dept.LayananDepartemenID
AND tarif.KelasID = 'Default'
AND tarif.NA = 'N'
LEFT JOIN masterlayanan AS layanan
ON layanan.LayananID = layanan_dept.LayananID
LEFT JOIN jenislayanan AS jenis
ON jenis.JenisLayananID = layanan.JenisLayananID
WHERE layanan_dept.NA = 'N'
AND layanan_dept.LayananID IN (/*SLICE:layanan_ids*/?)
AND (
? IS NULL
OR layanan_dept.LayananID LIKE ?
OR layanan.Nama LIKE ?
)
ORDER BY layanan.Nama, layanan.JenisLayananID
`
type MappingHargaPeriode10Params struct {
LayananIds []sql.NullString `json:"layanan_ids"`
Search sql.NullString `json:"search"`
}
type MappingHargaPeriode10Row struct {
Tariflayananid int64 `json:"tariflayananid"`
Layananid sql.NullString `json:"layananid"`
Namalayanan sql.NullString `json:"namalayanan"`
Jenislayananid sql.NullInt64 `json:"jenislayananid"`
Namajenislayanan sql.NullString `json:"namajenislayanan"`
Kelas string `json:"kelas"`
Namakelas string `json:"namakelas"`
Harga sql.NullString `json:"harga"`
Tipediskonrj string `json:"tipediskonrj"`
Diskonrj string `json:"diskonrj"`
Tipediskonri string `json:"tipediskonri"`
Diskonri string `json:"diskonri"`
Tipeshare sql.NullString `json:"tipeshare"`
Sharedokter sql.NullString `json:"sharedokter"`
Sharers sql.NullString `json:"sharers"`
Shareext sql.NullString `json:"shareext"`
Sharelain sql.NullString `json:"sharelain"`
Asaltarif string `json:"asaltarif"`
}
// sqlc does not support dynamic table identifiers. This query is a generation
// template for one concrete period table; duplicate or generate per period as
// needed, then keep the HTTP response mapping unchanged.
func (q *Queries) MappingHargaPeriode10(ctx context.Context, arg MappingHargaPeriode10Params) ([]MappingHargaPeriode10Row, error) {
query := mappingHargaPeriode10
var queryParams []interface{}
if len(arg.LayananIds) > 0 {
for _, v := range arg.LayananIds {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:layanan_ids*/?", strings.Repeat(",?", len(arg.LayananIds))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:layanan_ids*/?", "NULL", 1)
}
queryParams = append(queryParams, arg.Search)
queryParams = append(queryParams, arg.Search)
queryParams = append(queryParams, arg.Search)
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []MappingHargaPeriode10Row
for rows.Next() {
var i MappingHargaPeriode10Row
if err := rows.Scan(
&i.Tariflayananid,
&i.Layananid,
&i.Namalayanan,
&i.Jenislayananid,
&i.Namajenislayanan,
&i.Kelas,
&i.Namakelas,
&i.Harga,
&i.Tipediskonrj,
&i.Diskonrj,
&i.Tipediskonri,
&i.Diskonri,
&i.Tipeshare,
&i.Sharedokter,
&i.Sharers,
&i.Shareext,
&i.Sharelain,
&i.Asaltarif,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

134
internal/http/handler.go Normal file
View File

@@ -0,0 +1,134 @@
package http
import (
"context"
"errors"
"net/http"
"regexp"
"strings"
"primaya-api/cpone-middleware/internal/repository"
)
var digitsOnly = regexp.MustCompile(`^\d+$`)
type LayananRepository interface {
MappingHarga(ctx context.Context, params repository.MappingHargaParams) ([]repository.TarifLayanan, error)
}
type Handler struct {
repo LayananRepository
}
func NewHandler(repo LayananRepository) Handler {
return Handler{repo: repo}
}
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
layananIDs := parseLayananIDs(r)
if len(layananIDs) == 0 {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false,
Message: "Parameter layananID wajib diisi dan dapat berupa satu atau beberapa LayananID",
Data: nil,
})
return
}
periodeTarifID, ok := firstQueryValue(r, "periodeTarifID", "periode_tarif_id")
if !ok || strings.TrimSpace(periodeTarifID) == "" {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false,
Message: "Parameter periodeTarifID wajib diisi",
Data: nil,
})
return
}
periodeTarifID = strings.TrimSpace(periodeTarifID)
if !digitsOnly.MatchString(periodeTarifID) {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false,
Message: "Parameter periodeTarifID harus berupa angka",
Data: nil,
})
return
}
rows, err := h.repo.MappingHarga(r.Context(), repository.MappingHargaParams{
PeriodeTarifID: periodeTarifID,
LayananIDs: layananIDs,
Search: strings.TrimSpace(r.URL.Query().Get("search")),
})
if err != nil {
if errors.Is(err, repository.ErrTarifTableNotFound) {
WriteJSON(w, http.StatusNotFound, Response{
Success: false,
Message: "Tabel tarif dasar tidak ditemukan: harga_layanan_" + periodeTarifID,
Data: nil,
})
return
}
WriteJSON(w, http.StatusInternalServerError, Response{
Success: false,
Message: "internal server error",
Data: nil,
})
return
}
WriteJSON(w, http.StatusOK, Response{
Success: true,
Message: "ok",
Data: MappingHargaData{
PeriodeTarifID: periodeTarifID,
DetailLayanan: rows,
},
})
}
func parseLayananIDs(r *http.Request) []string {
values := queryValues(r, "layananID", "layananID[]")
if len(values) == 0 {
values = queryValues(r, "layanan_id", "layanan_id[]")
}
seen := make(map[string]struct{}, len(values))
ids := make([]string, 0, len(values))
for _, value := range values {
for _, part := range strings.Split(value, ",") {
id := strings.TrimSpace(part)
if id == "" {
continue
}
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
}
return ids
}
func queryValues(r *http.Request, keys ...string) []string {
query := r.URL.Query()
values := make([]string, 0)
for _, key := range keys {
values = append(values, query[key]...)
}
return values
}
func firstQueryValue(r *http.Request, keys ...string) (string, bool) {
query := r.URL.Query()
for _, key := range keys {
if values, exists := query[key]; exists && len(values) > 0 {
return values[0], true
}
}
return "", false
}

View File

@@ -0,0 +1,30 @@
package http
import (
"net/http"
"strings"
)
func BearerAuth(staticToken string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if bearerToken(r.Header.Get("Authorization")) != staticToken {
WriteJSON(w, http.StatusUnauthorized, Response{
Success: false,
Message: "Unauthenticated.",
Data: nil,
})
return
}
next.ServeHTTP(w, r)
})
}
func bearerToken(header string) string {
const prefix = "Bearer "
if !strings.HasPrefix(header, prefix) {
return ""
}
return strings.TrimSpace(strings.TrimPrefix(header, prefix))
}

25
internal/http/response.go Normal file
View File

@@ -0,0 +1,25 @@
package http
import (
"encoding/json"
"net/http"
"primaya-api/cpone-middleware/internal/repository"
)
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data any `json:"data"`
}
type MappingHargaData struct {
PeriodeTarifID string `json:"PeriodeTarifID"`
DetailLayanan []repository.TarifLayanan `json:"DetailLayanan"`
}
func WriteJSON(w http.ResponseWriter, statusCode int, payload Response) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(payload)
}

View File

@@ -0,0 +1,240 @@
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
)
var ErrTarifTableNotFound = errors.New("tarif table not found")
type MappingHargaParams struct {
PeriodeTarifID string
LayananIDs []string
Search string
}
type TarifLayanan struct {
TarifLayananID int64 `json:"TarifLayananID"`
LayananID *string `json:"LayananID"`
NamaLayanan *string `json:"NamaLayanan"`
JenisLayananID *int64 `json:"JenisLayananID"`
NamaJenisLayanan *string `json:"NamaJenisLayanan"`
Kelas string `json:"Kelas"`
NamaKelas string `json:"NamaKelas"`
Harga *string `json:"Harga"`
TipeDiskonRJ string `json:"TipeDiskonRJ"`
DiskonRJ string `json:"DiskonRJ"`
TipeDiskonRI string `json:"TipeDiskonRI"`
DiskonRI string `json:"DiskonRI"`
TipeShare *string `json:"TipeShare"`
ShareDokter *string `json:"ShareDokter"`
ShareRS *string `json:"ShareRS"`
ShareExt *string `json:"ShareExt"`
ShareLain *string `json:"ShareLain"`
AsalTarif string `json:"AsalTarif"`
}
type MySQLLayananRepository struct {
db *sql.DB
}
func NewMySQLLayananRepository(db *sql.DB) MySQLLayananRepository {
return MySQLLayananRepository{db: db}
}
func (r MySQLLayananRepository) MappingHarga(ctx context.Context, params MappingHargaParams) ([]TarifLayanan, error) {
tableName := "harga_layanan_" + params.PeriodeTarifID
exists, err := r.tableExists(ctx, tableName)
if err != nil {
return nil, err
}
if !exists {
return nil, ErrTarifTableNotFound
}
query, args := mappingHargaQuery(tableName, params.LayananIDs, params.Search)
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]TarifLayanan, 0)
seenLayananID := map[string]struct{}{}
for rows.Next() {
row, err := scanTarifLayanan(rows)
if err != nil {
return nil, err
}
layananID := stringValue(row.LayananID)
if layananID != "" {
if _, exists := seenLayananID[layananID]; exists {
continue
}
seenLayananID[layananID] = struct{}{}
}
result = append(result, row)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
func (r MySQLLayananRepository) tableExists(ctx context.Context, tableName string) (bool, error) {
var count int
err := r.db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = ?
`, tableName).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func mappingHargaQuery(tableName string, layananIDs []string, search string) (string, []any) {
args := make([]any, 0, len(layananIDs)+2)
placeholders := make([]string, 0, len(layananIDs))
for _, id := range layananIDs {
placeholders = append(placeholders, "?")
args = append(args, id)
}
var builder strings.Builder
builder.WriteString(`
SELECT
tarif.HargaLayananID AS TarifLayananID,
layanan_dept.LayananID,
layanan.Nama AS NamaLayanan,
layanan.JenisLayananID,
jenis.Nama AS NamaJenisLayanan,
'Default' AS Kelas,
'Default' AS NamaKelas,
tarif.Harga,
'Percentage' AS TipeDiskonRJ,
'0.00' AS DiskonRJ,
'Percentage' AS TipeDiskonRI,
'0.00' AS DiskonRI,
tarif.ShareTipe AS TipeShare,
tarif.ShareDokter,
tarif.ShareRS,
tarif.ShareExternal AS ShareExt,
tarif.ShareLain,
'+Layanan' AS AsalTarif
FROM layanandepartemen AS layanan_dept
INNER JOIN `)
builder.WriteString(tableName)
builder.WriteString(` AS tarif
ON tarif.LayananDepartemenID = layanan_dept.LayananDepartemenID
AND tarif.KelasID = 'Default'
AND tarif.NA = 'N'
LEFT JOIN masterlayanan AS layanan
ON layanan.LayananID = layanan_dept.LayananID
LEFT JOIN jenislayanan AS jenis
ON jenis.JenisLayananID = layanan.JenisLayananID
WHERE layanan_dept.NA = 'N'
AND layanan_dept.LayananID IN (` + strings.Join(placeholders, ",") + `)
`)
if search != "" {
like := "%" + search + "%"
builder.WriteString(`
AND (
layanan_dept.LayananID LIKE ?
OR layanan.Nama LIKE ?
)
`)
args = append(args, like, like)
}
builder.WriteString(`
ORDER BY layanan.Nama, layanan.JenisLayananID
`)
return builder.String(), args
}
func scanTarifLayanan(rows *sql.Rows) (TarifLayanan, error) {
var row TarifLayanan
var layananID sql.NullString
var namaLayanan sql.NullString
var jenisLayananID sql.NullInt64
var namaJenisLayanan sql.NullString
var harga sql.NullString
var tipeShare sql.NullString
var shareDokter sql.NullString
var shareRS sql.NullString
var shareExt sql.NullString
var shareLain sql.NullString
err := rows.Scan(
&row.TarifLayananID,
&layananID,
&namaLayanan,
&jenisLayananID,
&namaJenisLayanan,
&row.Kelas,
&row.NamaKelas,
&harga,
&row.TipeDiskonRJ,
&row.DiskonRJ,
&row.TipeDiskonRI,
&row.DiskonRI,
&tipeShare,
&shareDokter,
&shareRS,
&shareExt,
&shareLain,
&row.AsalTarif,
)
if err != nil {
return TarifLayanan{}, fmt.Errorf("scan tarif layanan: %w", err)
}
row.LayananID = nullStringPtr(layananID)
row.NamaLayanan = nullStringPtr(namaLayanan)
row.JenisLayananID = nullInt64Ptr(jenisLayananID)
row.NamaJenisLayanan = nullStringPtr(namaJenisLayanan)
row.Harga = nullStringPtr(harga)
row.TipeShare = nullStringPtr(tipeShare)
row.ShareDokter = nullStringPtr(shareDokter)
row.ShareRS = nullStringPtr(shareRS)
row.ShareExt = nullStringPtr(shareExt)
row.ShareLain = nullStringPtr(shareLain)
return row, nil
}
func nullStringPtr(value sql.NullString) *string {
if !value.Valid {
return nil
}
return &value.String
}
func nullInt64Ptr(value sql.NullInt64) *int64 {
if !value.Valid {
return nil
}
return &value.Int64
}
func stringValue(value *string) string {
if value == nil {
return ""
}
return *value
}

11
sqlc.yaml Normal file
View File

@@ -0,0 +1,11 @@
version: "2"
sql:
- engine: "mysql"
schema: "sqlc/schema.sql"
queries: "sqlc/query.sql"
gen:
go:
package: "db"
out: "internal/db"
emit_json_tags: true
emit_prepared_queries: false

41
sqlc/query.sql Normal file
View File

@@ -0,0 +1,41 @@
-- sqlc does not support dynamic table identifiers. This query is a generation
-- template for one concrete period table; duplicate or generate per period as
-- needed, then keep the HTTP response mapping unchanged.
--
-- name: MappingHargaPeriode10 :many
SELECT
tarif.HargaLayananID AS TarifLayananID,
layanan_dept.LayananID,
layanan.Nama AS NamaLayanan,
layanan.JenisLayananID,
jenis.Nama AS NamaJenisLayanan,
'Default' AS Kelas,
'Default' AS NamaKelas,
tarif.Harga,
'Percentage' AS TipeDiskonRJ,
'0.00' AS DiskonRJ,
'Percentage' AS TipeDiskonRI,
'0.00' AS DiskonRI,
tarif.ShareTipe AS TipeShare,
tarif.ShareDokter,
tarif.ShareRS,
tarif.ShareExternal AS ShareExt,
tarif.ShareLain,
'+Layanan' AS AsalTarif
FROM layanandepartemen AS layanan_dept
INNER JOIN harga_layanan_10 AS tarif
ON tarif.LayananDepartemenID = layanan_dept.LayananDepartemenID
AND tarif.KelasID = 'Default'
AND tarif.NA = 'N'
LEFT JOIN masterlayanan AS layanan
ON layanan.LayananID = layanan_dept.LayananID
LEFT JOIN jenislayanan AS jenis
ON jenis.JenisLayananID = layanan.JenisLayananID
WHERE layanan_dept.NA = 'N'
AND layanan_dept.LayananID IN (sqlc.slice('layanan_ids'))
AND (
sqlc.narg('search') IS NULL
OR layanan_dept.LayananID LIKE sqlc.narg('search')
OR layanan.Nama LIKE sqlc.narg('search')
)
ORDER BY layanan.Nama, layanan.JenisLayananID;

29
sqlc/schema.sql Normal file
View File

@@ -0,0 +1,29 @@
CREATE TABLE layanandepartemen (
LayananDepartemenID BIGINT NOT NULL,
LayananID VARCHAR(50),
NA CHAR(1)
);
CREATE TABLE masterlayanan (
LayananID VARCHAR(50) NOT NULL,
Nama VARCHAR(255),
JenisLayananID BIGINT
);
CREATE TABLE jenislayanan (
JenisLayananID BIGINT NOT NULL,
Nama VARCHAR(255)
);
CREATE TABLE harga_layanan_10 (
HargaLayananID BIGINT NOT NULL,
LayananDepartemenID BIGINT,
KelasID VARCHAR(50),
NA CHAR(1),
Harga DECIMAL(18,2),
ShareTipe VARCHAR(50),
ShareDokter DECIMAL(18,2),
ShareRS DECIMAL(18,2),
ShareExternal DECIMAL(18,2),
ShareLain DECIMAL(18,2)
);