Compare commits

..

No commits in common. "main" and "development" have entirely different histories.

69 changed files with 186 additions and 3059 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
**/.env
!**/.env.example
**/*.exe
**/*.test
**/*.out
.git
.gitignore
.claude
docs

13
.gitignore vendored
View File

@ -1,7 +1,8 @@
*.env .go-cache/
!.env.example *.log
.env
.DS_Store
*.exe # compiled binary from `go build`
*.test /omnix-sopiga
*.out /omnix-sopiga.exe
vendor/

View File

@ -4,11 +4,20 @@ stages:
variables: variables:
GO_VERSION: "1.25" GO_VERSION: "1.25"
# TODO: confirm final remote URL once go-dw-framework is pushed, and set up
# a deploy token / CI_JOB_TOKEN with read access if the repo is private.
FRAMEWORK_REPO_URL: "https://repository.promas.id/prana/go-dw-framework.git"
.go-cache: # go.mod currently has `replace repository.promas.id/prana/go-dw-framework => ../go-dw-framework`
# for pre-release local development. CI clones it into that exact relative
# path so the replace directive resolves. Once go-dw-framework is tagged
# (e.g. v0.1.0), remove the replace line from go.mod, drop this clone step,
# and let `go mod download` fetch the tagged version normally.
.with-framework:
image: golang:${GO_VERSION}-alpine image: golang:${GO_VERSION}-alpine
before_script: before_script:
- apk add --no-cache git - apk add --no-cache git
- git clone --depth 1 "$FRAMEWORK_REPO_URL" ../go-dw-framework
cache: cache:
key: go-mod-cache key: go-mod-cache
paths: paths:
@ -16,68 +25,27 @@ variables:
variables: variables:
GOPATH: "$CI_PROJECT_DIR/.go-cache" GOPATH: "$CI_PROJECT_DIR/.go-cache"
# ---------- omnix-broadcast ---------- test:
test:omnix-broadcast:
stage: test stage: test
extends: .go-cache extends: .with-framework
script: script:
- cd services/omnix-broadcast
- go vet ./... - go vet ./...
- go test ./tests/... -v - go test ./tests/... -v
rules:
- changes:
- framework/**/*
- services/omnix-broadcast/**/*
- go.work
- go.work.sum
build:omnix-broadcast: # NOTE: assumes the GitLab project path checks out into a folder literally
# named `omnix-sopiga` (matches CI_PROJECT_DIR's basename) so the Dockerfile's
# `COPY omnix-sopiga ./omnix-sopiga` line lines up. Adjust if your project slug differs.
build:
stage: build stage: build
image: docker:24 image: docker:24
services: services:
- docker:24-dind - docker:24-dind
before_script:
- apk add --no-cache git
- git clone --depth 1 "$FRAMEWORK_REPO_URL" ../go-dw-framework
script: script:
- docker build -f deployments/docker/omnix-broadcast/Dockerfile - docker build -f Dockerfile -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" ..
-t "$CI_REGISTRY_IMAGE/omnix-broadcast:$CI_COMMIT_SHORT_SHA" .
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- docker push "$CI_REGISTRY_IMAGE/omnix-broadcast:$CI_COMMIT_SHORT_SHA" - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
rules: rules:
- if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_BRANCH == "main"'
changes:
- framework/**/*
- services/omnix-broadcast/**/*
- go.work
- go.work.sum
# ---------- template-service ----------
test:template-service:
stage: test
extends: .go-cache
script:
- cd services/template-service
- go vet ./...
- go test ./tests/... -v
rules:
- changes:
- framework/**/*
- services/template-service/**/*
- go.work
- go.work.sum
build:template-service:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -f deployments/docker/template-service/Dockerfile
-t "$CI_REGISTRY_IMAGE/template-service:$CI_COMMIT_SHORT_SHA" .
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- docker push "$CI_REGISTRY_IMAGE/template-service:$CI_COMMIT_SHORT_SHA"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- framework/**/*
- services/template-service/**/*
- go.work
- go.work.sum

View File

@ -1,17 +0,0 @@
# GitLab Code Owners — https://docs.gitlab.com/ee/user/project/codeowners/
# Update the group handles below to match your actual GitLab groups/usernames.
[Platform / Framework]
framework/ @platform-team
go.work
go.work.sum
[Omnix Broadcast Team]
services/omnix-broadcast/ @team-omnix-broadcast
[Template Service Team]
services/template-service/ @team-template-service
[Deployment]
deployments/ @platform-team
docker-compose.yml @platform-team

36
Dockerfile Normal file
View File

@ -0,0 +1,36 @@
# Build context: PARENT directory of this repo, i.e. the folder that
# contains both `omnix-sopiga/` and `go-dw-framework/` as siblings:
#
# docker build -f omnix-sopiga/Dockerfile -t omnix-sopiga ..
#
# Why: go.mod still uses a local `replace` directive pointing at
# ../go-dw-framework (see go.mod) for pre-release development, since
# go-dw-framework isn't tagged/published yet. Once it is tagged (e.g. v0.1.0)
# and reachable from the Docker build environment:
# 1. go.mod: remove the `replace` line, keep `require ... v0.1.0`
# 2. This Dockerfile: build context becomes just this repo (`.`), and the
# COPY lines below collapse to `COPY . .`
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
COPY go-dw-framework ./go-dw-framework
COPY omnix-sopiga ./omnix-sopiga
WORKDIR /src/omnix-sopiga
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/omnix-sopiga .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata && \
adduser -D -u 10001 app
USER app
WORKDIR /app
COPY --from=builder /out/omnix-sopiga .
COPY omnix-sopiga/migrations ./migrations
EXPOSE 8081
ENTRYPOINT ["./omnix-sopiga"]

View File

@ -1,7 +1,11 @@
# omnix-broadcast # omnix-sopiga
Collection broadcast worker: mengirim invoice via WhatsApp (Sopiga Collar API) ke nasabah Gadai Mulia. Collection broadcast worker: mengirim invoice via WhatsApp (Sopiga Collar API) ke nasabah Gadai Mulia.
> Repo standalone (bukan bagian monorepo). Dependency internal ada di
> [`go-dw-framework`](../go-dw-framework) — lihat `go.mod` untuk detail
> `replace` directive yang dipakai selama development lokal.
## Architecture ## Architecture
``` ```
@ -37,8 +41,10 @@ Tiga komponen jalan bersamaan (lihat `main.go`):
Template baru cukup didaftarkan lewat database (`sopiga_template_config` + Template baru cukup didaftarkan lewat database (`sopiga_template_config` +
`template_variable_mapping` + `sopiga_collar_config`) — tanpa redeploy kode. `template_variable_mapping` + `sopiga_collar_config`) — tanpa redeploy kode.
Lihat [docs/implementasi_guide.md](../../docs/implementasi_guide.md) dan Lihat [docs/implementasi_guide.md](docs/implementasi_guide.md) dan
[docs/quick_start.md](../../docs/quick_start.md) untuk panduan lengkap. [docs/quick_start.md](docs/quick_start.md) untuk panduan lengkap. Detail
permintaan integrasi webhook ke tim Omnix ada di
[docs/webhook_integration_request.md](docs/webhook_integration_request.md).
## Layers ## Layers
@ -73,6 +79,26 @@ Lihat [docs/implementasi_guide.md](../../docs/implementasi_guide.md) dan
| `WEBHOOK_PORT` | 8081 | | `WEBHOOK_PORT` | 8081 |
| `WEBHOOK_SECRET` | — (HMAC-SHA256 shared secret; kosongkan untuk skip verifikasi saat dev) | | `WEBHOOK_SECRET` | — (HMAC-SHA256 shared secret; kosongkan untuk skip verifikasi saat dev) |
## Development setup
Clone this repo and `go-dw-framework` as **siblings** (same parent folder) —
`go.mod` uses a local `replace` directive during pre-release development:
```
some-folder/
├── go-dw-framework/
└── omnix-sopiga/ ← you are here
```
```bash
git clone <go-dw-framework-remote> ../go-dw-framework
go build ./...
```
Once `go-dw-framework` is tagged (e.g. `v0.1.0`) and pushed to its remote,
switch `go.mod` to a plain `require repository.promas.id/prana/go-dw-framework v0.1.0`
(drop the `replace` line) and `go mod tidy`.
## Run ## Run
```bash ```bash
@ -81,7 +107,14 @@ psql -d gadai_mulia -f migrations/001_create_collection_broadcasts.up.sql
export DATABASE_DSN="postgres://user:pass@localhost:5432/gadai_mulia" export DATABASE_DSN="postgres://user:pass@localhost:5432/gadai_mulia"
export SOPIGA_TOKEN="your_api_token" export SOPIGA_TOKEN="your_api_token"
go run ./... go run .
```
## Docker build
```bash
# from the parent folder containing both omnix-sopiga/ and go-dw-framework/
docker build -f omnix-sopiga/Dockerfile -t omnix-sopiga ..
``` ```
## Test ## Test

View File

@ -9,7 +9,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/dto" "repository.promas.id/prana/omnix-sopiga/dto"
) )
type SopigaClient struct { type SopigaClient struct {

View File

@ -3,7 +3,7 @@ package main
import ( import (
"time" "time"
fwconfig "github.com/yourorg/go-dw-platform/framework/config" fwconfig "repository.promas.id/prana/go-dw-framework/config"
) )
type Config struct { type Config struct {

View File

@ -1,29 +0,0 @@
# Build context: repo root (../../..)
# docker build -f deployments/docker/omnix-broadcast/Dockerfile -t omnix-broadcast .
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
COPY go.work go.work.sum ./
COPY framework ./framework
COPY services/omnix-broadcast ./services/omnix-broadcast
COPY services/template-service ./services/template-service
WORKDIR /src/services/omnix-broadcast
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/omnix-broadcast .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata && \
adduser -D -u 10001 app
USER app
WORKDIR /app
COPY --from=builder /out/omnix-broadcast .
COPY services/omnix-broadcast/migrations ./migrations
EXPOSE 8081
ENTRYPOINT ["./omnix-broadcast"]

View File

@ -1,29 +0,0 @@
# Build context: repo root (../../..)
# docker build -f deployments/docker/template-service/Dockerfile -t template-service .
FROM golang:1.25-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
COPY go.work go.work.sum ./
COPY framework ./framework
COPY services/omnix-broadcast ./services/omnix-broadcast
COPY services/template-service ./services/template-service
WORKDIR /src/services/template-service
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/template-service .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata && \
adduser -D -u 10001 app
USER app
WORKDIR /app
COPY --from=builder /out/template-service .
COPY services/template-service/migrations ./migrations
EXPOSE 8080
ENTRYPOINT ["./template-service"]

View File

@ -1,22 +0,0 @@
services:
omnix-broadcast:
build:
context: .
dockerfile: deployments/docker/omnix-broadcast/Dockerfile
image: int-omnix/omnix-broadcast:latest
restart: unless-stopped
env_file:
- services/omnix-broadcast/.env
ports:
- "8081:8081"
template-service:
build:
context: .
dockerfile: deployments/docker/template-service/Dockerfile
image: int-omnix/template-service:latest
restart: unless-stopped
env_file:
- services/template-service/.env
ports:
- "8080:8080"

View File

@ -1,75 +0,0 @@
package config
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// LoadDotEnv reads KEY=VALUE pairs from path (default ".env") and applies
// them via os.Setenv, without overriding variables already set in the
// environment. Missing file is not an error — real env vars always win.
func LoadDotEnv(path string) error {
if path == "" {
path = ".env"
}
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
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.Trim(strings.TrimSpace(value), `"'`)
if _, exists := os.LookupEnv(key); !exists {
os.Setenv(key, value)
}
}
return scanner.Err()
}
func GetString(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func GetInt(key string, fallback int) int {
v := os.Getenv(key)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
return fallback
}
return n
}
func MustGetString(key string) (string, error) {
v := os.Getenv(key)
if v == "" {
return "", fmt.Errorf("missing required environment variable: %s", key)
}
return v, nil
}

View File

@ -1,30 +0,0 @@
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func ExecBatch(ctx context.Context, pool *pgxpool.Pool, stmt string, args [][]any) (int64, error) {
batch := &pgx.Batch{}
for _, a := range args {
batch.Queue(stmt, a...)
}
results := pool.SendBatch(ctx, batch)
defer results.Close()
var rowsAffected int64
for i := 0; i < len(args); i++ {
tag, err := results.Exec()
if err != nil {
return rowsAffected, fmt.Errorf("exec batch item %d: %w", i, err)
}
rowsAffected += tag.RowsAffected()
}
return rowsAffected, nil
}

View File

@ -1,30 +0,0 @@
package db
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Config struct {
DSN string
MaxConns int32
MinConns int32
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
}
func NewPool(ctx context.Context, cfg Config) (*pgxpool.Pool, error) {
poolCfg, err := pgxpool.ParseConfig(cfg.DSN)
if err != nil {
return nil, err
}
poolCfg.MaxConns = cfg.MaxConns
poolCfg.MinConns = cfg.MinConns
poolCfg.MaxConnLifetime = cfg.MaxConnLifetime
poolCfg.MaxConnIdleTime = cfg.MaxConnIdleTime
return pgxpool.NewWithConfig(ctx, poolCfg)
}

View File

@ -1,26 +0,0 @@
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(ctx context.Context) error) error {
tx, err := pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
if err := fn(ctx); err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit tx: %w", err)
}
return nil
}

View File

@ -1,61 +0,0 @@
module github.com/yourorg/go-dw-platform/framework
go 1.25.0
require (
github.com/gin-gonic/gin v1.12.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.5.0
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.3.0
go.opentelemetry.io/otel v1.44.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

View File

@ -1,146 +0,0 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,62 +0,0 @@
package ingestion
import (
"sync"
"time"
)
type Batcher[T any] struct {
mu sync.Mutex
items []T
size int
timeout time.Duration
createdAt time.Time
}
func NewBatcher[T any](size int, timeout time.Duration) *Batcher[T] {
return &Batcher[T]{
items: make([]T, 0, size),
size: size,
timeout: timeout,
createdAt: time.Now(),
}
}
func (b *Batcher[T]) Add(item T) {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.items) == 0 {
b.createdAt = time.Now()
}
b.items = append(b.items, item)
}
func (b *Batcher[T]) IsFull() bool {
b.mu.Lock()
defer b.mu.Unlock()
return len(b.items) >= b.size
}
func (b *Batcher[T]) IsExpired() bool {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.items) == 0 {
return false
}
return time.Since(b.createdAt) >= b.timeout
}
func (b *Batcher[T]) Get() []T {
b.mu.Lock()
defer b.mu.Unlock()
items := make([]T, len(b.items))
copy(items, b.items)
return items
}
func (b *Batcher[T]) Clear() {
b.mu.Lock()
defer b.mu.Unlock()
b.items = b.items[:0]
}

View File

@ -1,33 +0,0 @@
package ingestion
import "sync"
type Deduplicator struct {
mu sync.RWMutex
seen map[string]struct{}
}
func NewDeduplicator() *Deduplicator {
return &Deduplicator{
seen: make(map[string]struct{}),
}
}
func (d *Deduplicator) Exists(key string) bool {
d.mu.RLock()
defer d.mu.RUnlock()
_, ok := d.seen[key]
return ok
}
func (d *Deduplicator) Mark(key string) {
d.mu.Lock()
defer d.mu.Unlock()
d.seen[key] = struct{}{}
}
func (d *Deduplicator) Reset() {
d.mu.Lock()
defer d.mu.Unlock()
d.seen = make(map[string]struct{})
}

View File

@ -1,41 +0,0 @@
package ingestion
import (
"context"
"time"
)
type Retrier struct {
MaxRetries int
BaseDelay time.Duration
}
func NewRetrier(maxRetries int, baseDelay time.Duration) *Retrier {
return &Retrier{
MaxRetries: maxRetries,
BaseDelay: baseDelay,
}
}
func (r *Retrier) Do(ctx context.Context, fn func() (int64, error)) (int64, error) {
var lastErr error
for attempt := 0; attempt <= r.MaxRetries; attempt++ {
if attempt > 0 {
delay := r.BaseDelay * time.Duration(1<<uint(attempt-1))
select {
case <-ctx.Done():
return 0, ctx.Err()
case <-time.After(delay):
}
}
result, err := fn()
if err == nil {
return result, nil
}
lastErr = err
}
return 0, lastErr
}

View File

@ -1,20 +0,0 @@
package logger
import (
"log/slog"
"os"
)
type Logger struct {
*slog.Logger
}
func New(module string) *Logger {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
base := slog.New(handler).With("module", module)
return &Logger{Logger: base}
}

View File

@ -1,60 +0,0 @@
package metrics
import (
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
},
[]string{"method", "endpoint", "status_code"},
)
dbQueryDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "db_query_duration_seconds",
Help: "Database query duration in seconds",
},
[]string{"operation"},
)
batchInsertRows = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "batch_insert_rows",
Help: "Number of rows inserted per batch",
},
)
cacheHits = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "cache_hits_total",
Help: "Total number of cache hits",
},
[]string{"key"},
)
)
func init() {
prometheus.MustRegister(httpRequestDuration, dbQueryDuration, batchInsertRows, cacheHits)
}
func RecordHTTPRequest(method, endpoint, statusCode string, duration time.Duration) {
httpRequestDuration.WithLabelValues(method, endpoint, statusCode).Observe(duration.Seconds())
}
func RecordDatabaseQuery(operation string, duration time.Duration) {
dbQueryDuration.WithLabelValues(operation).Observe(duration.Seconds())
}
func RecordBatchInsert(rowCount int, duration time.Duration) {
batchInsertRows.Observe(float64(rowCount))
}
func RecordCacheHit(key string, duration time.Duration) {
cacheHits.WithLabelValues(key).Inc()
}

View File

@ -1,48 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{
"success": false,
"message": "missing authorization token",
})
c.Abort()
return
}
// TODO: verify token and set user context
c.Next()
}
}
func RoleMiddleware(required []string) gin.HandlerFunc {
return func(c *gin.Context) {
role := c.GetString("user_role")
allowed := false
for _, r := range required {
if r == role {
allowed = true
break
}
}
if !allowed {
c.JSON(403, gin.H{
"success": false,
"message": "insufficient permissions",
})
c.Abort()
return
}
c.Next()
}
}

View File

@ -1,36 +0,0 @@
package middleware
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yourorg/go-dw-platform/framework/logger"
"github.com/yourorg/go-dw-platform/framework/metrics"
)
func Logging(log *logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.NewString()
}
c.Set("request_id", requestID)
start := time.Now()
c.Next()
duration := time.Since(start)
log.Info("request handled",
"request_id", requestID,
"method", c.Request.Method,
"path", c.Request.URL.Path,
"status", c.Writer.Status(),
"duration_ms", duration.Milliseconds(),
)
metrics.RecordHTTPRequest(c.Request.Method, c.FullPath(), strconv.Itoa(c.Writer.Status()), duration)
}
}

View File

@ -1,24 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
func Tracing(serviceName string) gin.HandlerFunc {
tracer := otel.Tracer(serviceName)
return func(c *gin.Context) {
ctx, span := tracer.Start(c.Request.Context(), c.FullPath())
defer span.End()
span.SetAttributes(
attribute.String("http.method", c.Request.Method),
attribute.String("http.path", c.Request.URL.Path),
)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}

View File

@ -1,30 +0,0 @@
package query
const (
DefaultLimit = 100
MaxLimit = 10000
)
type Filter struct {
StartDate string
EndDate string
Limit int
Page int
}
func NormalizeLimit(limit int) int {
if limit <= 0 {
return DefaultLimit
}
if limit > MaxLimit {
return MaxLimit
}
return limit
}
func Offset(page, limit int) int {
if page <= 1 {
return 0
}
return (page - 1) * limit
}

View File

@ -1,37 +0,0 @@
package query
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
type Cache struct {
client *redis.Client
}
func NewCache(client *redis.Client) *Cache {
return &Cache{client: client}
}
func (c *Cache) Get(ctx context.Context, key string, dest any) error {
val, err := c.client.Get(ctx, key).Bytes()
if err != nil {
return err
}
return json.Unmarshal(val, dest)
}
func (c *Cache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, key, data, ttl).Err()
}
func (c *Cache) Invalidate(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}

20
go.mod Normal file
View File

@ -0,0 +1,20 @@
module repository.promas.id/prana/omnix-sopiga
go 1.25.0
require (
github.com/jackc/pgx/v5 v5.5.0
repository.promas.id/prana/go-dw-framework v0.0.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/stretchr/testify v1.8.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
)
replace repository.promas.id/prana/go-dw-framework => ../go-dw-framework

28
go.sum Normal file
View File

@ -0,0 +1,28 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,7 +0,0 @@
go 1.25.0
use (
./framework
./services/omnix-broadcast
./services/template-service
)

View File

@ -1,66 +0,0 @@
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -9,10 +9,10 @@ import (
"io" "io"
"net/http" "net/http"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger" fwlogger "repository.promas.id/prana/go-dw-framework/logger"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/dto" "repository.promas.id/prana/omnix-sopiga/dto"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service" "repository.promas.id/prana/omnix-sopiga/service"
) )
const signatureHeader = "X-Sopiga-Signature" const signatureHeader = "X-Sopiga-Signature"

View File

@ -8,17 +8,17 @@ import (
"syscall" "syscall"
"time" "time"
fwconfig "github.com/yourorg/go-dw-platform/framework/config" fwconfig "repository.promas.id/prana/go-dw-framework/config"
fwdb "github.com/yourorg/go-dw-platform/framework/db" fwdb "repository.promas.id/prana/go-dw-framework/db"
fwingestion "github.com/yourorg/go-dw-platform/framework/ingestion" fwingestion "repository.promas.id/prana/go-dw-framework/ingestion"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger" fwlogger "repository.promas.id/prana/go-dw-framework/logger"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/client" "repository.promas.id/prana/omnix-sopiga/client"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/handler" "repository.promas.id/prana/omnix-sopiga/handler"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/repository" "repository.promas.id/prana/omnix-sopiga/repository"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service" "repository.promas.id/prana/omnix-sopiga/service"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/transformer" "repository.promas.id/prana/omnix-sopiga/transformer"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/worker" "repository.promas.id/prana/omnix-sopiga/worker"
) )
func main() { func main() {

View File

@ -7,7 +7,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/entity" "repository.promas.id/prana/omnix-sopiga/entity"
) )
type Repository struct { type Repository struct {

View File

@ -4,15 +4,15 @@ import (
"context" "context"
"fmt" "fmt"
fwingestion "github.com/yourorg/go-dw-platform/framework/ingestion" fwingestion "repository.promas.id/prana/go-dw-framework/ingestion"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger" fwlogger "repository.promas.id/prana/go-dw-framework/logger"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/client" "repository.promas.id/prana/omnix-sopiga/client"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/dto" "repository.promas.id/prana/omnix-sopiga/dto"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/repository" "repository.promas.id/prana/omnix-sopiga/repository"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/transformer" "repository.promas.id/prana/omnix-sopiga/transformer"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/validator" "repository.promas.id/prana/omnix-sopiga/validator"
) )
type Service struct { type Service struct {

View File

@ -1,20 +0,0 @@
module github.com/yourorg/go-dw-platform/services/omnix-broadcast
go 1.21
require (
github.com/jackc/pgx/v5 v5.5.0
github.com/yourorg/go-dw-platform/framework v0.0.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/stretchr/testify v1.8.3 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.9.0 // indirect
)
replace github.com/yourorg/go-dw-platform/framework => ../../framework

View File

@ -1,11 +0,0 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -1,11 +0,0 @@
PORT=8080
DATABASE_DSN=postgres://user:pass@localhost:5432/gadai_mulia
DB_MAX_CONNS=25
DB_MIN_CONNS=5
BATCH_SIZE=5000
BATCH_TIMEOUT_SEC=30
BATCH_MAX_RETRIES=3
LOG_LEVEL=info

View File

@ -1,49 +0,0 @@
package main
import (
"time"
fwconfig "github.com/yourorg/go-dw-platform/framework/config"
)
type Config struct {
Port string
DB DBConfig
Batch BatchConfig
LogLevel string
}
type DBConfig struct {
DSN string
MaxConns int32
MinConns int32
MaxConnLifetime time.Duration
MaxConnIdleTime time.Duration
}
type BatchConfig struct {
Size int
TimeoutSec int
MaxRetries int
RetryDelay time.Duration
}
func LoadConfig() *Config {
return &Config{
Port: fwconfig.GetString("PORT", "8080"),
DB: DBConfig{
DSN: fwconfig.GetString("DATABASE_DSN", ""),
MaxConns: int32(fwconfig.GetInt("DB_MAX_CONNS", 25)),
MinConns: int32(fwconfig.GetInt("DB_MIN_CONNS", 5)),
MaxConnLifetime: 15 * time.Minute,
MaxConnIdleTime: 5 * time.Minute,
},
Batch: BatchConfig{
Size: fwconfig.GetInt("BATCH_SIZE", 5000),
TimeoutSec: fwconfig.GetInt("BATCH_TIMEOUT_SEC", 30),
MaxRetries: fwconfig.GetInt("BATCH_MAX_RETRIES", 3),
RetryDelay: time.Second,
},
LogLevel: fwconfig.GetString("LOG_LEVEL", "info"),
}
}

View File

@ -1,30 +0,0 @@
package domain
import (
"fmt"
"time"
)
type Order struct {
OrderID string
CustomerID string
Amount float64
CreatedAt time.Time
}
func (o *Order) Validate() error {
if o.OrderID == "" {
return fmt.Errorf("order_id is required")
}
if o.CustomerID == "" {
return fmt.Errorf("customer_id is required")
}
if o.Amount <= 0 {
return fmt.Errorf("amount must be positive")
}
return nil
}
func (o *Order) NaturalKey() string {
return fmt.Sprintf("%s_%s_%d", o.OrderID, o.CustomerID, o.CreatedAt.Unix())
}

View File

@ -1,7 +0,0 @@
package dto
type IngestRequest struct {
OrderID string `json:"order_id" binding:"required"`
CustomerID string `json:"customer_id" binding:"required"`
Amount float64 `json:"amount" binding:"required"`
}

View File

@ -1,20 +0,0 @@
package dto
type ProcessResult struct {
Status string `json:"status"`
RowsInserted int64 `json:"rows_inserted,omitempty"`
}
type SuccessResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
RequestID string `json:"request_id"`
}
type ErrorResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
ErrorCode string `json:"error_code,omitempty"`
RequestID string `json:"request_id,omitempty"`
}

View File

@ -1,10 +0,0 @@
package entity
import "time"
type Order struct {
OrderID string `db:"order_id"`
CustomerID string `db:"customer_id"`
Amount float64 `db:"amount"`
CreatedAt time.Time `db:"created_at"`
}

View File

@ -1,11 +0,0 @@
module github.com/yourorg/go-dw-platform/services/template-service
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/jackc/pgx/v5 v5.5.0
github.com/yourorg/go-dw-platform/framework v0.0.0
)
replace github.com/yourorg/go-dw-platform/framework => ../../framework

View File

@ -1,28 +0,0 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
)
type HealthHandler struct {
pool *pgxpool.Pool
}
func NewHealthHandler(pool *pgxpool.Pool) *HealthHandler {
return &HealthHandler{pool: pool}
}
func (h *HealthHandler) Health(c *gin.Context) {
dbOK := h.pool.Ping(c.Request.Context()) == nil
status := "healthy"
if !dbOK {
status = "degraded"
}
c.JSON(200, gin.H{
"status": status,
"database": dbOK,
})
}

View File

@ -1,71 +0,0 @@
package handler
import (
"errors"
"github.com/gin-gonic/gin"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
"github.com/yourorg/go-dw-platform/services/template-service/dto"
"github.com/yourorg/go-dw-platform/services/template-service/service"
)
type Handler struct {
service *service.Service
logger *fwlogger.Logger
}
func New(svc *service.Service, log *fwlogger.Logger) *Handler {
return &Handler{service: svc, logger: log}
}
func (h *Handler) IngestOrders(c *gin.Context) {
ctx := c.Request.Context()
requestID := c.GetString("request_id")
var req dto.IngestRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("validation failed",
"request_id", requestID,
"error", err)
c.JSON(400, dto.ErrorResponse{
Success: false,
Message: "validation failed",
ErrorCode: "VALIDATION_ERROR",
RequestID: requestID,
})
return
}
result, err := h.service.Process(ctx, &req)
if err != nil {
if errors.Is(err, service.ErrDuplicate) {
c.JSON(409, dto.ErrorResponse{
Success: false,
Message: "duplicate order",
ErrorCode: "DUPLICATE_ORDER",
RequestID: requestID,
})
return
}
h.logger.Error("process failed",
"request_id", requestID,
"error", err)
c.JSON(500, dto.ErrorResponse{
Success: false,
Message: "failed to process order",
ErrorCode: "INTERNAL_ERROR",
RequestID: requestID,
})
return
}
c.JSON(200, dto.SuccessResponse{
Success: true,
Message: "order processed successfully",
Data: result,
RequestID: requestID,
})
}

View File

@ -1,76 +0,0 @@
package main
import (
"context"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
fwdb "github.com/yourorg/go-dw-platform/framework/db"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
fwmiddleware "github.com/yourorg/go-dw-platform/framework/middleware"
"github.com/yourorg/go-dw-platform/services/template-service/handler"
"github.com/yourorg/go-dw-platform/services/template-service/repository"
"github.com/yourorg/go-dw-platform/services/template-service/service"
"github.com/yourorg/go-dw-platform/services/template-service/transformer"
)
func main() {
cfg := LoadConfig()
log := fwlogger.New("template-service")
ctx := context.Background()
pool, err := fwdb.NewPool(ctx, fwdb.Config{
DSN: cfg.DB.DSN,
MaxConns: cfg.DB.MaxConns,
MinConns: cfg.DB.MinConns,
MaxConnLifetime: cfg.DB.MaxConnLifetime,
MaxConnIdleTime: cfg.DB.MaxConnIdleTime,
})
if err != nil {
log.Error("failed to connect to database", "error", err)
os.Exit(1)
}
defer pool.Close()
repo := repository.New(pool)
tf := transformer.New()
svc := service.New(
repo,
tf,
cfg.Batch.Size,
time.Duration(cfg.Batch.TimeoutSec)*time.Second,
cfg.Batch.MaxRetries,
cfg.Batch.RetryDelay,
)
ingestHandler := handler.New(svc, log)
healthHandler := handler.NewHealthHandler(pool)
router := gin.New()
router.Use(fwmiddleware.Logging(log))
router.Use(gin.Recovery())
router.GET("/health", healthHandler.Health)
api := router.Group("/api")
api.Use(fwmiddleware.AuthMiddleware())
api.POST("/ingest", ingestHandler.IngestOrders)
go func() {
if err := router.Run(":" + cfg.Port); err != nil {
log.Error("server stopped", "error", err)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
log.Info("shutting down gracefully")
}

View File

@ -1 +0,0 @@
DROP TABLE IF EXISTS orders;

View File

@ -1,11 +0,0 @@
CREATE TABLE IF NOT EXISTS orders (
id BIGSERIAL PRIMARY KEY,
order_id VARCHAR(64) NOT NULL,
customer_id VARCHAR(64) NOT NULL,
amount NUMERIC(18, 2) NOT NULL,
natural_key VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_orders_customer_created
ON orders (customer_id, created_at);

View File

@ -1,103 +0,0 @@
package repository
import (
"context"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/yourorg/go-dw-platform/services/template-service/entity"
)
type Filter struct {
StartDate time.Time
EndDate time.Time
Limit int
}
type Repository struct {
pool *pgxpool.Pool
}
func New(pool *pgxpool.Pool) *Repository {
return &Repository{pool: pool}
}
func (r *Repository) ExistsByKey(ctx context.Context, naturalKey string) (bool, error) {
var exists bool
err := r.pool.QueryRow(ctx,
`SELECT EXISTS(SELECT 1 FROM orders WHERE natural_key = $1)`,
naturalKey,
).Scan(&exists)
if err != nil {
return false, fmt.Errorf("exists by key: %w", err)
}
return exists, nil
}
func (r *Repository) InsertBatch(ctx context.Context, entities []*entity.Order) (int64, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return 0, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
stmt := `INSERT INTO orders (order_id, customer_id, amount, natural_key, created_at)
VALUES ($1, $2, $3, $4, $5)`
batch := &pgx.Batch{}
for _, e := range entities {
naturalKey := fmt.Sprintf("%s_%s_%d", e.OrderID, e.CustomerID, e.CreatedAt.Unix())
batch.Queue(stmt, e.OrderID, e.CustomerID, e.Amount, naturalKey, e.CreatedAt)
}
results := tx.SendBatch(ctx, batch)
var rowsInserted int64
for i := 0; i < len(entities); i++ {
tag, err := results.Exec()
if err != nil {
results.Close()
return 0, fmt.Errorf("exec batch: %w", err)
}
rowsInserted += tag.RowsAffected()
}
if err := results.Close(); err != nil {
return 0, fmt.Errorf("close batch results: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return 0, fmt.Errorf("commit: %w", err)
}
return rowsInserted, nil
}
func (r *Repository) FindByFilters(ctx context.Context, filter *Filter) ([]*entity.Order, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
query := `SELECT order_id, customer_id, amount, created_at
FROM orders
WHERE created_at >= $1 AND created_at <= $2
LIMIT $3`
rows, err := r.pool.Query(ctx, query, filter.StartDate, filter.EndDate, filter.Limit)
if err != nil {
return nil, fmt.Errorf("query: %w", err)
}
defer rows.Close()
var orders []*entity.Order
for rows.Next() {
var o entity.Order
if err := rows.Scan(&o.OrderID, &o.CustomerID, &o.Amount, &o.CreatedAt); err != nil {
return nil, fmt.Errorf("scan: %w", err)
}
orders = append(orders, &o)
}
return orders, rows.Err()
}

View File

@ -1,83 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"time"
fwingestion "github.com/yourorg/go-dw-platform/framework/ingestion"
fwmetrics "github.com/yourorg/go-dw-platform/framework/metrics"
"github.com/yourorg/go-dw-platform/services/template-service/domain"
"github.com/yourorg/go-dw-platform/services/template-service/dto"
"github.com/yourorg/go-dw-platform/services/template-service/entity"
"github.com/yourorg/go-dw-platform/services/template-service/repository"
"github.com/yourorg/go-dw-platform/services/template-service/transformer"
)
var ErrDuplicate = errors.New("duplicate order")
type Service struct {
repository *repository.Repository
transformer *transformer.Transformer
batcher *fwingestion.Batcher[*domain.Order]
retrier *fwingestion.Retrier
}
func New(repo *repository.Repository, tf *transformer.Transformer, batchSize int, batchTimeout time.Duration, maxRetries int, retryDelay time.Duration) *Service {
return &Service{
repository: repo,
transformer: tf,
batcher: fwingestion.NewBatcher[*domain.Order](batchSize, batchTimeout),
retrier: fwingestion.NewRetrier(maxRetries, retryDelay),
}
}
func (s *Service) Process(ctx context.Context, req *dto.IngestRequest) (*dto.ProcessResult, error) {
order, err := s.transformer.RequestToDomain(req)
if err != nil {
return nil, fmt.Errorf("process: %w", err)
}
exists, err := s.repository.ExistsByKey(ctx, order.NaturalKey())
if err != nil {
return nil, fmt.Errorf("check duplicate: %w", err)
}
if exists {
return nil, ErrDuplicate
}
s.batcher.Add(order)
if s.batcher.IsFull() || s.batcher.IsExpired() {
return s.FlushBatch(ctx)
}
return &dto.ProcessResult{Status: "queued"}, nil
}
func (s *Service) FlushBatch(ctx context.Context) (*dto.ProcessResult, error) {
start := time.Now()
batch := s.batcher.Get()
entities := make([]*entity.Order, len(batch))
for i, d := range batch {
entities[i] = s.transformer.DomainToEntity(d)
}
rows, err := s.retrier.Do(ctx, func() (int64, error) {
return s.repository.InsertBatch(ctx, entities)
})
if err != nil {
return nil, fmt.Errorf("flush batch: %w", err)
}
fwmetrics.RecordBatchInsert(len(batch), time.Since(start))
s.batcher.Clear()
return &dto.ProcessResult{
Status: "success",
RowsInserted: rows,
}, nil
}

View File

@ -1,16 +0,0 @@
package tests
import (
"time"
"github.com/yourorg/go-dw-platform/services/template-service/domain"
)
func NewTestOrder(orderID, customerID string, amount float64) *domain.Order {
return &domain.Order{
OrderID: orderID,
CustomerID: customerID,
Amount: amount,
CreatedAt: time.Now(),
}
}

View File

@ -1,26 +0,0 @@
package tests
import (
"testing"
"github.com/yourorg/go-dw-platform/services/template-service/dto"
"github.com/yourorg/go-dw-platform/services/template-service/validator"
)
func TestValidateIngestRequest_MissingOrderID(t *testing.T) {
req := &dto.IngestRequest{CustomerID: "cust-1", Amount: 100}
err := validator.ValidateIngestRequest(req)
if err != validator.ErrMissingOrderID {
t.Fatalf("expected ErrMissingOrderID, got %v", err)
}
}
func TestValidateIngestRequest_Valid(t *testing.T) {
req := &dto.IngestRequest{OrderID: "ord-1", CustomerID: "cust-1", Amount: 100}
if err := validator.ValidateIngestRequest(req); err != nil {
t.Fatalf("expected no error, got %v", err)
}
}

View File

@ -1,40 +0,0 @@
package transformer
import (
"fmt"
"time"
"github.com/yourorg/go-dw-platform/services/template-service/domain"
"github.com/yourorg/go-dw-platform/services/template-service/dto"
"github.com/yourorg/go-dw-platform/services/template-service/entity"
)
type Transformer struct{}
func New() *Transformer {
return &Transformer{}
}
func (t *Transformer) RequestToDomain(req *dto.IngestRequest) (*domain.Order, error) {
order := &domain.Order{
OrderID: req.OrderID,
CustomerID: req.CustomerID,
Amount: req.Amount,
CreatedAt: time.Now(),
}
if err := order.Validate(); err != nil {
return nil, fmt.Errorf("transform: %w", err)
}
return order, nil
}
func (t *Transformer) DomainToEntity(d *domain.Order) *entity.Order {
return &entity.Order{
OrderID: d.OrderID,
CustomerID: d.CustomerID,
Amount: d.Amount,
CreatedAt: d.CreatedAt,
}
}

View File

@ -1,22 +0,0 @@
package validator
import (
"errors"
"github.com/yourorg/go-dw-platform/services/template-service/dto"
)
var ErrMissingOrderID = errors.New("order_id is required")
func ValidateIngestRequest(req *dto.IngestRequest) error {
if req.OrderID == "" {
return ErrMissingOrderID
}
if req.CustomerID == "" {
return errors.New("customer_id is required")
}
if req.Amount <= 0 {
return errors.New("amount must be positive")
}
return nil
}

View File

@ -3,8 +3,8 @@ package tests
import ( import (
"testing" "testing"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/transformer" "repository.promas.id/prana/omnix-sopiga/transformer"
) )
func TestBuildDynamicMessage_Ordered(t *testing.T) { func TestBuildDynamicMessage_Ordered(t *testing.T) {

View File

@ -3,8 +3,8 @@ package tests
import ( import (
"testing" "testing"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/validator" "repository.promas.id/prana/omnix-sopiga/validator"
) )
func TestValidateBroadcastPayload_MissingPhone(t *testing.T) { func TestValidateBroadcastPayload_MissingPhone(t *testing.T) {

View File

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/entity" "repository.promas.id/prana/omnix-sopiga/entity"
) )
type Transformer struct{} type Transformer struct{}

View File

@ -3,7 +3,7 @@ package validator
import ( import (
"errors" "errors"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
) )
var ( var (

View File

@ -4,10 +4,10 @@ import (
"context" "context"
"time" "time"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger" fwlogger "repository.promas.id/prana/go-dw-framework/logger"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service" "repository.promas.id/prana/omnix-sopiga/service"
) )
const maxConcurrency = 5 const maxConcurrency = 5

View File

@ -4,10 +4,10 @@ import (
"context" "context"
"time" "time"
fwlogger "github.com/yourorg/go-dw-platform/framework/logger" fwlogger "repository.promas.id/prana/go-dw-framework/logger"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain" "repository.promas.id/prana/omnix-sopiga/domain"
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service" "repository.promas.id/prana/omnix-sopiga/service"
) )
type DeliverySyncWorker struct { type DeliverySyncWorker struct {