package repository import ( "context" "database/sql" "errors" "fmt" "strconv" "strings" "github.com/go-sql-driver/mysql" ) type TarifKerjasamaListParams struct { Search string PerusahaanID string TarifID *int64 Page int PerPage int } type TarifKerjasamaDetailParams struct { GroupTarifID *int64 Active string EffectiveDate string } type TarifKerjasamaPage struct { Items []TarifKerjasamaSummary Total int } type TarifKerjasamaSummary struct { PerusahaanTarifID int64 `json:"PerusahaanTarifID"` PerusahaanID string `json:"PerusahaanID"` KodePerusahaan *string `json:"KodePerusahaan"` NamaPerusahaan string `json:"NamaPerusahaan"` PIC *string `json:"PIC"` Alamat *string `json:"Alamat"` Telepon *string `json:"Telepon"` Email *string `json:"Email"` JumlahGroupTarif int `json:"JumlahGroupTarif"` } type TarifKerjasamaDetail struct { TarifKerjasamaSummary GroupTarif []TarifKerjasamaGroup `json:"GroupTarif"` } type TarifKerjasamaGroup struct { GroupTarifID int64 `json:"GroupTarifID"` PerusahaanTarifID int64 `json:"PerusahaanTarifID"` NamaGroupTarif string `json:"NamaGroupTarif"` TanggalBerlaku string `json:"TanggalBerlaku"` TanggalBerakhir string `json:"TanggalBerakhir"` PeriodeTarifID *int64 `json:"PeriodeTarifID"` KelasDefault *string `json:"KelasDefault"` SemuaLayanan *string `json:"SemuaLayanan"` DiskonSemuaLayananRJ *float64 `json:"DiskonSemuaLayananRJ"` DiskonSemuaLayananRI *float64 `json:"DiskonSemuaLayananRI"` Keterangan *string `json:"Keterangan"` NA string `json:"NA"` Aktif bool `json:"Aktif"` JumlahLayanan int `json:"JumlahLayanan"` Layanan []TarifKerjasamaLayanan `json:"Layanan"` } type TarifKerjasamaLayanan struct { LayananID *string `json:"LayananID"` NamaLayanan *string `json:"NamaLayanan"` JenisLayananID *string `json:"JenisLayananID"` NamaJenisLayanan *string `json:"NamaJenisLayanan"` JumlahDetail int `json:"JumlahDetail"` Detail []TarifKerjasamaLayananDetail `json:"Detail"` } type TarifKerjasamaLayananDetail struct { TarifLayananID int64 `json:"TarifLayananID"` KelasID *string `json:"KelasID"` NamaKelas *string `json:"NamaKelas"` Harga float64 `json:"Harga"` TipeDiskonRJ *string `json:"TipeDiskonRJ"` DiskonRJ float64 `json:"DiskonRJ"` TipeDiskonRI *string `json:"TipeDiskonRI"` DiskonRI float64 `json:"DiskonRI"` TipeShare *string `json:"TipeShare"` ShareDokter float64 `json:"ShareDokter"` ShareRS float64 `json:"ShareRS"` ShareExternal float64 `json:"ShareExternal"` ShareLain float64 `json:"ShareLain"` AsalTarif *string `json:"AsalTarif"` } func (r MySQLLayananRepository) ListTarifKerjasama(ctx context.Context, params TarifKerjasamaListParams) (TarifKerjasamaPage, error) { where, args := tarifKerjasamaListWhere(params) var total int countQuery := `SELECT COUNT(*) FROM perusahaan_tarif pt INNER JOIN perusahaan p ON p.PerusahaanID = pt.PerusahaanID ` + where if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil { return TarifKerjasamaPage{}, err } query := `SELECT pt.Perusahaan_TarifID, pt.PerusahaanID, p.KodePerusahaan, p.Nama, pt.PenanggungJawab, p.Alamat, pt.TeleponPenanggungJawab, pt.EmailPenanggungJawab, COALESCE(pg.JumlahGroupTarif, 0) FROM perusahaan_tarif pt INNER JOIN perusahaan p ON p.PerusahaanID = pt.PerusahaanID LEFT JOIN ( SELECT Perusahaan_TarifID, COUNT(*) AS JumlahGroupTarif FROM perusahaan_grouptarif WHERE NA = 'N' GROUP BY Perusahaan_TarifID ) pg ON pg.Perusahaan_TarifID = pt.Perusahaan_TarifID ` + where + ` ORDER BY p.Nama, pt.Perusahaan_TarifID LIMIT ? OFFSET ?` queryArgs := append(append([]any{}, args...), params.PerPage, (params.Page-1)*params.PerPage) rows, err := r.db.QueryContext(ctx, query, queryArgs...) if err != nil { return TarifKerjasamaPage{}, err } defer rows.Close() items := make([]TarifKerjasamaSummary, 0) for rows.Next() { item, err := scanTarifKerjasamaSummary(rows) if err != nil { return TarifKerjasamaPage{}, err } items = append(items, item) } if err := rows.Err(); err != nil { return TarifKerjasamaPage{}, err } return TarifKerjasamaPage{Items: items, Total: total}, nil } func tarifKerjasamaListWhere(params TarifKerjasamaListParams) (string, []any) { conditions := []string{"p.NA = 'N'"} args := make([]any, 0, 6) if params.Search != "" { like := "%" + params.Search + "%" conditions = append(conditions, `(p.Nama LIKE ? OR p.KodePerusahaan LIKE ? OR pt.PerusahaanID LIKE ? OR pt.PenanggungJawab LIKE ?)`) args = append(args, like, like, like, like) } if params.PerusahaanID != "" { conditions = append(conditions, "pt.PerusahaanID = ?") args = append(args, params.PerusahaanID) } if params.TarifID != nil { conditions = append(conditions, "pt.Perusahaan_TarifID = ?") args = append(args, *params.TarifID) } return "WHERE " + strings.Join(conditions, " AND "), args } func (r MySQLLayananRepository) GetTarifKerjasama(ctx context.Context, id int64, params TarifKerjasamaDetailParams) (*TarifKerjasamaDetail, error) { row := r.db.QueryRowContext(ctx, `SELECT pt.Perusahaan_TarifID, pt.PerusahaanID, p.KodePerusahaan, p.Nama, pt.PenanggungJawab, p.Alamat, pt.TeleponPenanggungJawab, pt.EmailPenanggungJawab, 0 FROM perusahaan_tarif pt INNER JOIN perusahaan p ON p.PerusahaanID = pt.PerusahaanID WHERE pt.Perusahaan_TarifID = ? AND p.NA = 'N' LIMIT 1`, id) master, err := scanTarifKerjasamaSummary(row) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } groups, err := r.tarifKerjasamaGroups(ctx, id, params) if err != nil { return nil, err } master.JumlahGroupTarif = len(groups) return &TarifKerjasamaDetail{TarifKerjasamaSummary: master, GroupTarif: groups}, nil } func scanTarifKerjasamaSummary(row rowScanner) (TarifKerjasamaSummary, error) { var item TarifKerjasamaSummary var kode, pic, alamat, telepon, email sql.NullString err := row.Scan(&item.PerusahaanTarifID, &item.PerusahaanID, &kode, &item.NamaPerusahaan, &pic, &alamat, &telepon, &email, &item.JumlahGroupTarif) item.KodePerusahaan = nullStringPtr(kode) item.PIC = nullStringPtr(pic) item.Alamat = nullStringPtr(alamat) item.Telepon = nullStringPtr(telepon) item.Email = nullStringPtr(email) return item, err } func (r MySQLLayananRepository) tarifKerjasamaGroups(ctx context.Context, id int64, params TarifKerjasamaDetailParams) ([]TarifKerjasamaGroup, error) { if params.Active == "" { params.Active = "N" } conditions := []string{"pg.Perusahaan_TarifID = ?", "pg.NA = ?"} args := []any{id, params.Active} if params.GroupTarifID != nil { conditions = append(conditions, "pg.GroupTarifID = ?") args = append(args, *params.GroupTarifID) } if params.EffectiveDate != "" { conditions = append(conditions, "DATE(pg.TanggalBerlaku) <= ?", "DATE(pg.TanggalBerakhir) >= ?") args = append(args, params.EffectiveDate, params.EffectiveDate) } query := `SELECT pg.GroupTarifID, pg.Perusahaan_TarifID, pg.Keterangan, CAST(pg.TanggalBerlaku AS CHAR), CAST(pg.TanggalBerakhir AS CHAR), pg.PeriodeTarifID, pg.KelasDefault, pg.SemuaLayanan, CAST(pg.HargaLayanan AS CHAR), CAST(pg.HargaLayananRI AS CHAR), pg.NA FROM perusahaan_grouptarif pg WHERE ` + strings.Join(conditions, " AND ") + ` ORDER BY pg.TanggalBerlaku DESC, pg.GroupTarifID DESC` rows, err := r.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() groups := make([]TarifKerjasamaGroup, 0) for rows.Next() { var group TarifKerjasamaGroup var keterangan, kelasDefault, semuaLayanan, hargaRJ, hargaRI sql.NullString var periodeTarifID sql.NullInt64 if err := rows.Scan( &group.GroupTarifID, &group.PerusahaanTarifID, &keterangan, &group.TanggalBerlaku, &group.TanggalBerakhir, &periodeTarifID, &kelasDefault, &semuaLayanan, &hargaRJ, &hargaRI, &group.NA, ); err != nil { return nil, err } group.Keterangan = nullStringPtr(keterangan) group.KelasDefault = nullStringPtr(kelasDefault) group.SemuaLayanan = nullStringPtr(semuaLayanan) group.PeriodeTarifID = nullInt64Ptr(periodeTarifID) group.DiskonSemuaLayananRJ = nullDecimalPtr(hargaRJ) group.DiskonSemuaLayananRI = nullDecimalPtr(hargaRI) group.NamaGroupTarif = "Group Tarif " + strconv.FormatInt(group.GroupTarifID, 10) if group.Keterangan != nil && *group.Keterangan != "" { group.NamaGroupTarif = *group.Keterangan } group.Aktif = group.NA == "N" groups = append(groups, group) } if err := rows.Err(); err != nil { _ = rows.Close() return nil, err } if err := rows.Close(); err != nil { return nil, err } for index := range groups { groups[index].Layanan, err = r.tarifKerjasamaLayanan(ctx, groups[index].GroupTarifID) if err != nil { return nil, err } groups[index].JumlahLayanan = len(groups[index].Layanan) } return groups, nil } type tarifKerjasamaLayananRow struct { TarifLayananID int64 LayananID sql.NullString NamaLayanan sql.NullString JenisLayananID sql.NullString NamaJenis sql.NullString KelasID sql.NullString NamaKelas sql.NullString Harga sql.NullString TipeDiskonRJ sql.NullString DiskonRJ sql.NullString TipeDiskonRI sql.NullString DiskonRI sql.NullString TipeShare sql.NullString ShareDokter sql.NullString ShareRS sql.NullString ShareExternal sql.NullString ShareLain sql.NullString AsalTarif sql.NullString } func (r MySQLLayananRepository) tarifKerjasamaLayanan(ctx context.Context, groupID int64) ([]TarifKerjasamaLayanan, error) { tableName := "perusahaan_tarif_layanan_" + strconv.FormatInt(groupID, 10) query := fmt.Sprintf(`SELECT tarif.TarifLayananID, CAST(tarif.LayananID AS CHAR), layanan.Nama, CAST(tarif.JenisLayananID AS CHAR), jenis.Nama, CAST(tarif.KelasID AS CHAR), kelas.Nama, CAST(tarif.Harga AS CHAR), tarif.TipeDiskon, CAST(tarif.Diskon AS CHAR), '' AS TipeDiskonRI, '0' AS DiskonRI, tarif.TipeShare, CAST(tarif.ShareDokter AS CHAR), CAST(tarif.ShareRS AS CHAR), CAST(tarif.ShareExternal AS CHAR), CAST(tarif.ShareLain AS CHAR), tarif.AsalTarif FROM %s tarif LEFT JOIN masterlayanan layanan ON layanan.LayananID = tarif.LayananID LEFT JOIN jenislayanan jenis ON jenis.JenisLayananID = tarif.JenisLayananID LEFT JOIN kelas kelas ON kelas.KelasID = tarif.KelasID WHERE tarif.NA = 'N' ORDER BY layanan.Nama, tarif.LayananID, tarif.KelasID`, tableName) rows, err := r.db.QueryContext(ctx, query) if err != nil { if isMySQLMissingTable(err) { return []TarifKerjasamaLayanan{}, nil } return nil, err } defer rows.Close() flatRows := make([]tarifKerjasamaLayananRow, 0) for rows.Next() { var row tarifKerjasamaLayananRow if err := rows.Scan( &row.TarifLayananID, &row.LayananID, &row.NamaLayanan, &row.JenisLayananID, &row.NamaJenis, &row.KelasID, &row.NamaKelas, &row.Harga, &row.TipeDiskonRJ, &row.DiskonRJ, &row.TipeDiskonRI, &row.DiskonRI, &row.TipeShare, &row.ShareDokter, &row.ShareRS, &row.ShareExternal, &row.ShareLain, &row.AsalTarif, ); err != nil { return nil, err } flatRows = append(flatRows, row) } if err := rows.Err(); err != nil { return nil, err } return groupTarifKerjasamaLayanan(flatRows), nil } func isMySQLMissingTable(err error) bool { var mysqlError *mysql.MySQLError return errors.As(err, &mysqlError) && mysqlError.Number == 1146 } func groupTarifKerjasamaLayanan(rows []tarifKerjasamaLayananRow) []TarifKerjasamaLayanan { result := make([]TarifKerjasamaLayanan, 0) indexes := make(map[string]int) for _, row := range rows { key := row.LayananID.String + "\x1f" + row.JenisLayananID.String index, exists := indexes[key] if !exists { index = len(result) indexes[key] = index result = append(result, TarifKerjasamaLayanan{ LayananID: nullStringPtr(row.LayananID), NamaLayanan: nullStringPtr(row.NamaLayanan), JenisLayananID: nullStringPtr(row.JenisLayananID), NamaJenisLayanan: nullStringPtr(row.NamaJenis), Detail: make([]TarifKerjasamaLayananDetail, 0), }) } namaKelas := row.NamaKelas if !namaKelas.Valid || namaKelas.String == "" { namaKelas = row.KelasID } result[index].Detail = append(result[index].Detail, TarifKerjasamaLayananDetail{ TarifLayananID: row.TarifLayananID, KelasID: nullStringPtr(row.KelasID), NamaKelas: nullStringPtr(namaKelas), Harga: decimalValue(row.Harga), TipeDiskonRJ: nullStringPtr(row.TipeDiskonRJ), DiskonRJ: decimalValue(row.DiskonRJ), TipeDiskonRI: nullStringPtr(row.TipeDiskonRI), DiskonRI: decimalValue(row.DiskonRI), TipeShare: nullStringPtr(row.TipeShare), ShareDokter: decimalValue(row.ShareDokter), ShareRS: decimalValue(row.ShareRS), ShareExternal: decimalValue(row.ShareExternal), ShareLain: decimalValue(row.ShareLain), AsalTarif: nullStringPtr(row.AsalTarif), }) result[index].JumlahDetail = len(result[index].Detail) } return result } func nullDecimalPtr(value sql.NullString) *float64 { if !value.Valid { return nil } parsed := decimalValue(value) return &parsed } func decimalValue(value sql.NullString) float64 { if !value.Valid { return 0 } parsed, _ := strconv.ParseFloat(value.String, 64) return parsed }