Compare commits

...

6 Commits

23 changed files with 875 additions and 14 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(helm template:*)"
]
}
}

177
README.md Normal file
View File

@@ -0,0 +1,177 @@
# Payouts Service
## API Endpoints
### User Management
#### Register User
- **Path**: `/api/v1/user/register`
- **Method**: POST
- **Request Parameters**:
```json
{
"tin": "string",
"phone": "string",
"password": "string",
"password_cfm": "string"
}
```
- **Response Parameters**:
```json
{
"status": "string"
}
```
- **Curl Example**:
```bash
curl -X POST http://localhost:8080/api/v1/user/register \
-H "Content-Type: application/json" \
-d '{"tin":"1234567890","phone":"+79991234567","password":"password123","password_cfm":"password123"}'
```
#### User Login
- **Path**: `/api/v1/user/login`
- **Method**: POST
- **Request Parameters**:
```json
{
"phone": "string",
"password": "string"
}
```
- **Response Parameters**:
```json
{
"token": "string",
"token_ttl": "integer"
}
```
- **Curl Example**:
```bash
curl -X POST http://localhost:8080/api/v1/user/login \
-H "Content-Type: application/json" \
-d '{"phone":"+79991234567","password":"password123"}'
```
### Payout Operations
#### Get SBP Banks
- **Path**: `/api/v1/payout/sbp/banks`
- **Method**: GET
- **Request Parameters**: None
- **Response Parameters**:
```json
[
{
"bank_id": "string",
"name": "string",
"bic": "string"
}
]
```
- **Curl Example**:
```bash
curl -X GET http://localhost:8080/api/v1/payout/sbp/banks
```
#### Create Payout
- **Path**: `/api/v1/payout/create`
- **Method**: POST
- **Request Parameters: SBP**:
```json
{
"payout_type": "spb",
"bank_id": "string",
"amount": "float"
}
```
> **_NOTE:_**
> Phone number for SBP payout comes from user's profile
- **Request Parameters: YooMoney**:
```json
{
"payout_type": "yoo_money",
"account_number": "string",
"amount": "float"
}
```
- **Response Parameters**:
```json
{
"payout_id": "string",
"payout_status": "string"
}
```
- **Curl Example**:
```bash
curl -X POST http://localhost:8080/api/v1/payout/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <session_token>" \
-d '{"payout_type":"spb","bank_id":"123456","amount":10.00}'
```
#### Payout Callback
- **Path**: `/api/v1/payout/callback`
- **Method**: POST
- **Request Parameters**: YooKassa webhook data (JSON)
- **Response Parameters**: None
- **Curl Example**:
```bash
curl -X POST http://localhost:8080/api/v1/payout/callback \
-H "Content-Type: application/json" \
-d '{"id":"payout_123456","status":"succeeded","amount":{"value":"10.00","currency":"RUB"}}'
```
## Service Configuration
### Server Configuration
| Property | Description | Default Value |
|----------|-------------|---------------|
| Server.Tls.Enabled | Enable TLS for the server | false |
| Server.Tls.CertFile | Path to TLS certificate file | (empty) |
| Server.Tls.KeyFile | Path to TLS key file | (empty) |
| Server.Port | Server port | :8080 |
| Server.WriteTimeout | Write timeout for requests | 35s |
| Server.ReadTimeout | Read timeout for requests | 35s |
| Server.EnablePProfEndpoints | Enable pprof debug endpoints | false |
### Database Configuration
| Property | Description | Default Value |
|----------|-------------|---------------|
| Database.Type | Database type (sqlite, postgres) | (empty) |
| Database.Connection | Database connection string | (empty) |
| Database.LogLevel | Database logging level | Info |
| Database.TraceRequests | Enable request tracing | false |
### YooKassa Configuration
| Property | Description | Default Value |
|----------|-------------|---------------|
| YooKassa.Test | Enable test mode | false |
| YooKassa.ApiBaseKey | YooKassa base API key | (empty) |
| YooKassa.ApiBaseSecret | YooKassa base API secret | (empty) |
| YooKassa.ApiPaymentKey | YooKassa payment API key | (empty) |
| YooKassa.ApiPaymentSecret | YooKassa payment API secret | (empty) |
| YooKassa.BaseUrl | YooKassa API base URL | https://api.yookassa.ru/v3 |
| YooKassa.Timeout | Request timeout | 2s |
| YooKassa.CheckAllowedCallbackAddress | Check callback IP addresses | true |
| YooKassa.AllowedCallbackSubnets | Allowed callback IP subnets | (list of allowed subnets) |
| YooKassa.CallbackProcessTimeout | Delay to process YooKassa allback | 1s |
| YooKassa.Retry.Enabled | Enable request retries | false |
| YooKassa.Retry.Count | Number of retry attempts | 3 |
| YooKassa.Retry.WaitTime | Initial wait time between retries | 200ms |
| YooKassa.Retry.MaxWaitTime | Maximum wait time between retries | 5s |
### Cache Configuration
| Property | Description | Default Value |
|----------|-------------|---------------|
| Cache.TTL | Session TTL | 24h |
### Logging Configuration
| Property | Description | Default Value |
|----------|-------------|---------------|
| Log.Level | Log level | DEBUG |
| Log.FilePath | | ./logs/payouts.log
| Log.TextOutput | | false
| Log.StdoutEnabled | | true
| Log.FileEnabled | | false

View File

@@ -3,10 +3,9 @@ Server.WriteTimeout = 35s
Server.ReadTimeout = 35s Server.ReadTimeout = 35s
Server.EnablePProfEndpoints = false Server.EnablePProfEndpoints = false
Socket.MaxHttpBufferSize = 2097152 Server.Tls.Enabled = false
Socket.PingInterval = 25s Server.Tls.CertFile =
Socket.PingTimeout = 20s Server.Tls.KeyFile =
Socket.Debug = false
# Prometheus settings # Prometheus settings
Metrics.Endpoint = /metrics Metrics.Endpoint = /metrics
@@ -60,4 +59,4 @@ YooKassa.ApiBaseSecret =
YooKassa.ApiPaymentKey = YooKassa.ApiPaymentKey =
YooKassa.ApiPaymentSecret = YooKassa.ApiPaymentSecret =
# Timeout to process yookassa callback # Timeout to process yookassa callback
YooKassa.CallbackProcessTimeout = 1s YooKassa.CallbackProcessTimeout = 1s

6
helm/payouts/Chart.yaml Normal file
View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: payouts
description: Payouts service Helm chart
type: application
version: 0.1.0
appVersion: "1.0.0"

203
helm/payouts/README.md Normal file
View File

@@ -0,0 +1,203 @@
# payouts Helm Chart
Helm chart for deploying the **payouts** service to Kubernetes.
## Prerequisites
- Kubernetes 1.21+
- Helm 3.2+
## Installing the Chart
```bash
helm install payouts ./helm
```
To install into a specific namespace:
```bash
helm install payouts ./helm --namespace payouts --create-namespace
```
## Uninstalling the Chart
```bash
helm uninstall payouts
```
## Configuration
All parameters are set via `values.yaml` or `--set` flags.
### Image
| Parameter | Description | Default |
|-----------|-------------|---------|
| `image.repository` | Container image repository | `payouts` |
| `image.tag` | Container image tag | `latest` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
### Service
| Parameter | Description | Default |
|-----------|-------------|---------|
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | Service port | `8080` |
### Ingress
| Parameter | Description | Default |
|-----------|-------------|---------|
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | IngressClass name | `""` |
| `ingress.annotations` | Ingress annotations | `{}` |
| `ingress.host` | Ingress hostname | `payouts.example.com` |
| `ingress.path` | Ingress path | `/` |
| `ingress.pathType` | Ingress path type | `Prefix` |
| `ingress.tls` | TLS configuration | `[]` |
### TLS (application-level)
When `config.Server.Tls.Enabled` is `true` the chart mounts a TLS secret as
files into the pod at the paths defined by `config.Server.Tls.CertFile` and
`config.Server.Tls.KeyFile`.
Two modes are supported:
**Option A — use an existing secret** (type `kubernetes.io/tls`):
```yaml
config:
Server:
Tls:
Enabled: true
tls:
existingSecret: "my-tls-secret"
```
**Option B — let the chart create the secret** (supply PEM values, do not commit to VCS):
```yaml
config:
Server:
Tls:
Enabled: true
tls:
cert: |
-----BEGIN CERTIFICATE-----
...
key: |
-----BEGIN PRIVATE KEY-----
...
```
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.Server.Tls.Enabled` | Enable TLS on the HTTP server | `false` |
| `config.Server.Tls.CertFile` | Path to the certificate file inside the pod | `/etc/payouts/tls/tls.crt` |
| `config.Server.Tls.KeyFile` | Path to the private key file inside the pod | `/etc/payouts/tls/tls.key` |
| `tls.existingSecret` | Name of an existing `kubernetes.io/tls` secret to use | `""` |
| `tls.cert` | PEM-encoded certificate (used when `existingSecret` is empty) | `""` |
| `tls.key` | PEM-encoded private key (used when `existingSecret` is empty) | `""` |
> When TLS is enabled, either `tls.existingSecret` or both `tls.cert` and `tls.key` must be provided — the chart will fail with a descriptive error otherwise.
### Application Config
Non-secret application parameters are stored in a ConfigMap and mounted as
`/etc/payouts/config.yaml` inside the pod. The path is passed to the application
via the `CONFIG_PATH` environment variable.
All keys preserve the exact casing from `config/payouts.properties`.
| Parameter | Description | Default |
|-----------|-------------|---------|
| `config.Server.Port` | HTTP listen address | `:8080` |
| `config.Server.WriteTimeout` | HTTP write timeout | `35s` |
| `config.Server.ReadTimeout` | HTTP read timeout | `35s` |
| `config.Server.EnablePProfEndpoints` | Expose pprof endpoints | `false` |
| `config.Socket.MaxHttpBufferSize` | Max HTTP buffer size for socket | `2097152` |
| `config.Socket.PingInterval` | Socket ping interval | `25s` |
| `config.Socket.PingTimeout` | Socket ping timeout | `20s` |
| `config.Socket.Debug` | Enable socket debug logging | `false` |
| `config.Metrics.Endpoint` | Prometheus metrics endpoint | `/metrics` |
| `config.Metrics.HistogramBuckets` | Global histogram buckets | `0.001,...,10` |
| `config.Metrics.Http.HistogramEnabled` | Enable HTTP latency histogram | `true` |
| `config.Metrics.Http.Buckets` | HTTP histogram buckets | `0.001,...,10` |
| `config.Log.Level` | Log level | `DEBUG` |
| `config.Log.FilePath` | Log file path | `./logs/payouts.log` |
| `config.Log.TextOutput` | Use plain-text log format | `false` |
| `config.Log.StdoutEnabled` | Log to stdout | `true` |
| `config.Log.FileEnabled` | Log to file | `false` |
| `config.Database.Type` | Database type (`sqlite` or `postgres`) | `""` |
| `config.Database.LogLevel` | Database query log level | `Info` |
| `config.Database.TraceRequests` | Trace all DB requests | `false` |
| `config.Cache.TTL` | Session cache TTL | `24h` |
| `config.YooKassa.BaseUrl` | YooKassa API base URL | `https://api.yookassa.ru/v3` |
| `config.YooKassa.Timeout` | YooKassa request timeout | `2s` |
| `config.YooKassa.Retry.Enabled` | Enable request retries | `false` |
| `config.YooKassa.Retry.Count` | Retry count (incl. initial) | `3` |
| `config.YooKassa.Retry.WaitTime` | Wait time between retries | `200ms` |
| `config.YooKassa.Retry.MaxWaitTime` | Max wait time (exponential backoff) | `5s` |
| `config.YooKassa.Test` | Enable YooKassa test mode | `false` |
| `config.YooKassa.CheckAllowedCallbackAddress` | Validate callback source IP | `true` |
| `config.YooKassa.AllowedCallbackSubnets` | Allowed callback CIDR list | YooKassa subnets |
| `config.YooKassa.CallbackProcessTimeout` | Timeout to process callback | `1s` |
### Secrets
Secret values are stored in a Kubernetes Secret and injected as environment
variables. Variable names are the uppercased property key with `.` replaced by `_`.
> **These values are empty by default and must be supplied before deploying to production.**
| Env variable | Original property |
|---|---|
| `DATABASE_CONNECTION` | `Database.Connection` |
| `YOOKASSA_APIBASEKEY` | `YooKassa.ApiBaseKey` |
| `YOOKASSA_APIBASESECRET` | `YooKassa.ApiBaseSecret` |
| `YOOKASSA_APIPAYMENTKEY` | `YooKassa.ApiPaymentKey` |
| `YOOKASSA_APIPAYMENTSECRET` | `YooKassa.ApiPaymentSecret` |
Provide secrets at install/upgrade time:
```bash
helm install payouts ./helm \
--set secrets.DATABASE_CONNECTION="host=127.0.0.1 user=app password=s3cr3t dbname=payouts port=5432 sslmode=disable" \
--set secrets.YOOKASSA_APIBASEKEY="<key>" \
--set secrets.YOOKASSA_APIBASESECRET="<secret>" \
--set secrets.YOOKASSA_APIPAYMENTKEY="<key>" \
--set secrets.YOOKASSA_APIPAYMENTSECRET="<secret>"
```
Or keep them in a separate values file that is **not committed to version control**:
```bash
helm install payouts ./helm -f secrets.values.yaml
```
Example `secrets.values.yaml`:
```yaml
secrets:
DATABASE_CONNECTION: "host=127.0.0.1 user=app password=s3cr3t dbname=payouts port=5432 sslmode=disable"
YOOKASSA_APIBASEKEY: "<key>"
YOOKASSA_APIBASESECRET: "<secret>"
YOOKASSA_APIPAYMENTKEY: "<key>"
YOOKASSA_APIPAYMENTSECRET: "<secret>"
```
### Ingress example
```yaml
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
host: payouts.example.com
tls:
- secretName: payouts-tls
hosts:
- payouts.example.com
```

View File

@@ -0,0 +1,45 @@
1. Get the application URL:
{{- if .Values.ingress.enabled }}
http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}{{ .Values.ingress.path }}
{{- else if eq .Values.service.type "NodePort" }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "payouts.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo "http://$NODE_IP:$NODE_PORT"
{{- else if eq .Values.service.type "LoadBalancer" }}
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "payouts.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo "http://$SERVICE_IP:{{ .Values.service.port }}"
{{- else }}
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "payouts.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
echo "http://127.0.0.1:{{ .Values.service.port }}"
{{- end }}
2. Configuration is mounted at $CONFIG_PATH inside the pod:
/etc/payouts/config.yaml (from ConfigMap {{ include "payouts.fullname" . }})
{{- if .Values.config.Server.Tls.Enabled }}
3. TLS is ENABLED. Certificate and key are mounted from Secret {{ include "payouts.tlsSecretName" . }}:
{{ .Values.config.Server.Tls.CertFile }}
{{ .Values.config.Server.Tls.KeyFile }}
{{- if not .Values.tls.existingSecret }}
The chart created the TLS secret. To rotate the certificate, update tls.cert / tls.key and run helm upgrade.
{{- else }}
Using existing secret: {{ .Values.tls.existingSecret }}
{{- end }}
4. Secret environment variables are injected from Secret {{ include "payouts.fullname" . }}:
{{- else }}
3. Secret environment variables are injected from Secret {{ include "payouts.fullname" . }}:
{{- end }}
DATABASE_CONNECTION, YOOKASSA_APIBASEKEY, YOOKASSA_APIBASESECRET,
YOOKASSA_APIPAYMENTKEY, YOOKASSA_APIPAYMENTSECRET
Before deploying to production, populate these values:
helm upgrade {{ .Release.Name }} ./helm \
--set secrets.DATABASE_CONNECTION="host=... dbname=..." \
--set secrets.YOOKASSA_APIBASEKEY="<key>" \
--set secrets.YOOKASSA_APIBASESECRET="<secret>" \
--set secrets.YOOKASSA_APIPAYMENTKEY="<key>" \
--set secrets.YOOKASSA_APIPAYMENTSECRET="<secret>"
Or use a separate values file that is not committed to version control:
helm upgrade {{ .Release.Name }} ./helm -f secrets.values.yaml

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "payouts.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "payouts.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "payouts.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{ include "payouts.selectorLabels" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "payouts.selectorLabels" -}}
app.kubernetes.io/name: {{ include "payouts.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Name of the TLS secret to mount.
Returns tls.existingSecret when set, otherwise the chart-managed secret name.
*/}}
{{- define "payouts.tlsSecretName" -}}
{{- if .Values.tls.existingSecret }}
{{- .Values.tls.existingSecret }}
{{- else }}
{{- printf "%s-tls" (include "payouts.fullname" .) }}
{{- end }}
{{- end }}
{{/*
Validate TLS configuration.
*/}}
{{- define "payouts.validateTls" -}}
{{- if .Values.config.Server.Tls.Enabled }}
{{- if and (not .Values.tls.existingSecret) (or (not .Values.tls.cert) (not .Values.tls.key)) }}
{{- fail "TLS is enabled: either set tls.existingSecret or provide both tls.cert and tls.key" }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "payouts.fullname" . }}
labels:
{{- include "payouts.labels" . | nindent 4 }}
data:
config.yaml: |
{{- .Values.config | toYaml | nindent 4 }}

View File

@@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "payouts.fullname" . }}
labels:
{{- include "payouts.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "payouts.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "payouts.selectorLabels" . | nindent 8 }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- if .Values.config.Server.Tls.Enabled }}
checksum/tls: {{ include (print $.Template.BasePath "/tls-secret.yaml") . | sha256sum }}
{{- end }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: CONFIG_PATH
value: /etc/payouts/config.yaml
{{- range $key, $value := .Values.secrets }}
- name: {{ $key }}
valueFrom:
secretKeyRef:
name: {{ include "payouts.fullname" $ }}
key: {{ $key }}
{{- end }}
ports:
- name: http
containerPort: {{ trimPrefix ":" .Values.config.Server.Port | int }}
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
volumeMounts:
- name: config
mountPath: /etc/payouts
readOnly: true
{{- if .Values.config.Server.Tls.Enabled }}
- name: tls
mountPath: /etc/payouts/tls
readOnly: true
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: config
configMap:
name: {{ include "payouts.fullname" . }}
{{- if .Values.config.Server.Tls.Enabled }}
- name: tls
secret:
secretName: {{ include "payouts.tlsSecretName" . }}
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,31 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "payouts.fullname" . }}
labels:
{{- include "payouts.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: {{ .Values.ingress.path }}
pathType: {{ .Values.ingress.pathType }}
backend:
service:
name: {{ include "payouts.fullname" . }}
port:
name: http
{{- end }}

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "payouts.fullname" . }}
labels:
{{- include "payouts.labels" . | nindent 4 }}
type: Opaque
data:
{{- range $key, $value := .Values.secrets }}
{{ $key }}: {{ $value | toString | b64enc | quote }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "payouts.fullname" . }}
labels:
{{- include "payouts.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "payouts.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,13 @@
{{- if and .Values.config.Server.Tls.Enabled (not .Values.tls.existingSecret) }}
{{- include "payouts.validateTls" . }}
apiVersion: v1
kind: Secret
metadata:
name: {{ printf "%s-tls" (include "payouts.fullname" .) }}
labels:
{{- include "payouts.labels" . | nindent 4 }}
type: kubernetes.io/tls
data:
tls.crt: {{ .Values.tls.cert | toString | b64enc }}
tls.key: {{ .Values.tls.key | toString | b64enc }}
{{- end }}

105
helm/payouts/values.yaml Normal file
View File

@@ -0,0 +1,105 @@
replicaCount: 1
image:
repository: payouts
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
className: ""
annotations: {}
host: payouts.example.com
path: /
pathType: Prefix
tls: []
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
# Non-secret application config — rendered as /etc/payouts/config.yaml inside the pod
config:
Server:
Port: ":8080"
WriteTimeout: 35s
ReadTimeout: 35s
EnablePProfEndpoints: false
Tls:
Enabled: false
CertFile: /etc/payouts/tls/tls.crt
KeyFile: /etc/payouts/tls/tls.key
Socket:
MaxHttpBufferSize: 2097152
PingInterval: 25s
PingTimeout: 20s
Debug: false
Metrics:
Endpoint: /metrics
HistogramBuckets: "0.001,0.002,0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10"
Http:
HistogramEnabled: true
Buckets: "0.001,0.002,0.005,0.01,0.025,0.05,0.1,0.25,0.5,1,2.5,5,10"
Log:
Level: DEBUG
FilePath: ./logs/payouts.log
TextOutput: false
StdoutEnabled: true
FileEnabled: false
Database:
Type: ""
LogLevel: Info
TraceRequests: false
Cache:
TTL: 24h
YooKassa:
BaseUrl: https://api.yookassa.ru/v3
Timeout: 2s
Retry:
Enabled: false
Count: 3
WaitTime: 200ms
MaxWaitTime: 5s
Test: false
CheckAllowedCallbackAddress: true
AllowedCallbackSubnets: "185.71.76.0/27,185.71.77.0/27,77.75.153.0/25,77.75.156.11/32,77.75.156.35/32,77.75.154.128/25,2a02:5180::/32"
CallbackProcessTimeout: 1s
# TLS for the application server
# When config.Server.Tls.Enabled is true, a volume with the cert and key is mounted
# into the pod at the paths defined by config.Server.Tls.CertFile / KeyFile.
#
# Option A — bring your own secret (must be type kubernetes.io/tls):
# tls.existingSecret: "my-tls-secret"
#
# Option B — supply PEM values and let the chart create the secret:
# tls.cert: |
# -----BEGIN CERTIFICATE-----
# ...
# tls.key: |
# -----BEGIN PRIVATE KEY-----
# ...
tls:
existingSecret: ""
cert: ""
key: ""
# Secret values — injected as env vars (uppercase, dots → underscores)
secrets:
DATABASE_CONNECTION: ""
YOOKASSA_APIBASEKEY: ""
YOOKASSA_APIBASESECRET: ""
YOOKASSA_APIPAYMENTKEY: ""
YOOKASSA_APIPAYMENTSECRET: ""

View File

@@ -0,0 +1,46 @@
package health
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"payouts/internal/service/database"
)
// Route health route
const Route = "/health"
// New constructs a new health Handler.
func New(dbService database.Service) (Handler, error) {
return &handler{
dbService: dbService,
}, nil
}
type handler struct {
dbService database.Service
}
// HealthHandler handles the health check requests
func (h *handler) Health(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
w.Header().Set("Content-Type", "application/json")
status := map[string]any{}
// Check database connection
err := h.dbService.HealthCheck()
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
status["Error"] = fmt.Sprintf("%v", err)
slog.Error("Health check failed", slog.String("error", status["Error"].(string)))
} else {
w.WriteHeader(http.StatusOK)
}
status["OK"] = (err == nil)
encoder := json.NewEncoder(w)
encoder.Encode(status)
}

View File

@@ -0,0 +1,16 @@
package health
import (
"net/http"
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(New),
)
// Handler health handler interface
type Handler interface {
Health(http.ResponseWriter, *http.Request)
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/fx" "go.uber.org/fx"
"payouts/internal/api/health"
"payouts/internal/api/payout" "payouts/internal/api/payout"
"payouts/internal/api/user" "payouts/internal/api/user"
"payouts/internal/api/version" "payouts/internal/api/version"
@@ -21,6 +22,7 @@ import (
// Module is a fx module // Module is a fx module
var Module = fx.Options( var Module = fx.Options(
user.Module, user.Module,
health.Module,
payout.Module, payout.Module,
version.Module, version.Module,
monitoring.Module, monitoring.Module,
@@ -40,6 +42,7 @@ type Params struct {
PayoutHandler payout.Handler PayoutHandler payout.Handler
UserHandler user.Handler UserHandler user.Handler
Version version.Handler Version version.Handler
HealthHandler health.Handler
Metrics monitoring.Metrics Metrics monitoring.Metrics
} }
@@ -50,7 +53,10 @@ func RegisterRoutes(p Params, lc fx.Lifecycle) {
router := mux.NewRouter() router := mux.NewRouter()
router.StrictSlash(true) router.StrictSlash(true)
// Version endpoint
router.HandleFunc(version.Route, p.Version.VersionHandler).Methods(http.MethodGet) router.HandleFunc(version.Route, p.Version.VersionHandler).Methods(http.MethodGet)
// Health check endpoint
router.HandleFunc(health.Route, p.HealthHandler.Health).Methods(http.MethodGet)
if p.AppConfig.Server.EnablePProfEndpoints { if p.AppConfig.Server.EnablePProfEndpoints {
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)

View File

@@ -3,6 +3,7 @@ package version
import ( import (
"io" "io"
"net/http" "net/http"
"runtime/debug"
"go.uber.org/fx" "go.uber.org/fx"
@@ -39,7 +40,20 @@ type handler struct {
func (h *handler) VersionHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) VersionHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
ver := h.version
if ver == "unknown" {
buildInfo, ok := debug.ReadBuildInfo()
if ok {
for _, setting := range buildInfo.Settings {
if setting.Key == "vcs.revision" {
ver = ver + "-" + setting.Value
break
}
}
}
}
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
io.WriteString(w, h.version) io.WriteString(w, ver+"\n")
} }

View File

@@ -66,6 +66,9 @@ func NewAppConfig() (*App, error) {
tempConf.SetConfigName(confName) tempConf.SetConfigName(confName)
tempConf.SetConfigType(confType) tempConf.SetConfigType(confType)
tempConf.AutomaticEnv()
tempConf.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
err := tempConf.ReadInConfig() err := tempConf.ReadInConfig()
if err != nil { if err != nil {
// complain on missed non-default config // complain on missed non-default config

View File

@@ -109,3 +109,17 @@ func (d *dbService) UpdatePayoutByPayoutID(payoutId string, updateModel orm.Payo
p := d.getParams(opts...) p := d.getParams(opts...)
return gorm.G[orm.Payout](d.db).Where("payout_id = ?", payoutId).Updates(p.ctx, updateModel) return gorm.G[orm.Payout](d.db).Where("payout_id = ?", payoutId).Updates(p.ctx, updateModel)
} }
// HealthCheck implements [Service].
func (d *dbService) HealthCheck() error {
if d.db == nil {
return errors.New("database connection is nil")
}
db, err := d.db.DB()
if err != nil {
return err
}
return db.Ping()
}

View File

@@ -31,6 +31,7 @@ type Service interface {
CreatePayout(payoutModel *orm.Payout, opts ...Optional) error CreatePayout(payoutModel *orm.Payout, opts ...Optional) error
UpdatePayoutById(id uint, updateModel orm.Payout, opts ...Optional) (int, error) UpdatePayoutById(id uint, updateModel orm.Payout, opts ...Optional) (int, error)
UpdatePayoutByPayoutID(payoutId string, updateModel orm.Payout, opts ...Optional) (int, error) UpdatePayoutByPayoutID(payoutId string, updateModel orm.Payout, opts ...Optional) (int, error)
HealthCheck() error
} }
// Params represents the module input params // Params represents the module input params

View File

@@ -1,10 +1,3 @@
/*
* Copyright (c) New Cloud Technologies, Ltd., 2013-2026
*
* You can not use the contents of the file in any way without New Cloud Technologies Ltd. written permission.
* To obtain such a permit, you should contact New Cloud Technologies, Ltd. at https://myoffice.ru/contacts/
*/
package config package config
import ( import (

View File

@@ -1 +1 @@
unknown unknown