All checks were successful
build-and-push / build (push) Successful in 39s
- config/db/storage/auth/router/perms: eQMS 규약 미러링, 권한 2-tier (관리자 전체 / 구성원 본인·신청만), oauth2-proxy 헤더 인증 + DEV_AUTH mock - 모델: 구성원/부서, 근무(출퇴근·휴가·공가·초과), 프로젝트(회사/제품/버전· 작업자portion·담당자·태스크·계약·첨부·분할입금), 인센티브(설정·단계· 유저배분·분기정산), 회계(거래·세금) - internal/worktime: 근로기준법 월 집계 엔진 - internal/incentive: BE/non-BE × 계약금/중도금/잔금 3단계 계산 + 시뮬레이션 - 시드 데이터, Go 멀티스테이지 Dockerfile - ADMIN_GROUPS 기본값 'admin' (전 내부 앱 공통 그룹) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
5.2 KiB
Go
113 lines
5.2 KiB
Go
package models
|
||
|
||
import (
|
||
"time"
|
||
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// Payment stage kinds (계약금/중도금/잔금) and scopes (BE / non-BE).
|
||
const (
|
||
StageDeposit = "deposit" // 계약금
|
||
StageMiddle = "middle" // 중도금
|
||
StageFinal = "final" // 잔금
|
||
|
||
ScopeBE = "be" // 손익분기 금액분 → 작업자 인센티브 포인트 풀
|
||
ScopeNonBE = "non_be" // BE 초과분 → 회사:파트너 분배
|
||
)
|
||
|
||
// Fix lifecycle for a user's incentive on a project stage:
|
||
// 예정 → 반영중(회사 입금) → 반영완료(포인트 반영) → 지급완료(급여 지급)
|
||
const (
|
||
FixPlanned = "planned" // 예정
|
||
FixApplying = "applying" // 반영중
|
||
FixApplied = "applied" // 반영완료
|
||
FixPaid = "paid" // 지급완료
|
||
)
|
||
|
||
// IncentiveConfig is the per-year rule set. Admin tunes it; once happy it gets
|
||
// frozen for the year. RankQuota maps rank label → annual point quota.
|
||
type IncentiveConfig struct {
|
||
Base
|
||
Year int `gorm:"uniqueIndex" json:"year"`
|
||
PointRate float64 `json:"pointRate"` // KRW per 1 incentive point (환율)
|
||
DepositPct float64 `json:"depositPct"` // 계약금 비율 (%)
|
||
MiddlePct float64 `json:"middlePct"` // 중도금 비율 (%)
|
||
FinalPct float64 `json:"finalPct"` // 잔금 비율 (%)
|
||
NonBECompanyPct float64 `json:"nonBeCompanyPct"` // non-BE 회사 몫 (%)
|
||
NonBEPartnerPct float64 `json:"nonBePartnerPct"` // non-BE 파트너 몫 (%)
|
||
RankQuota datatypes.JSONMap `json:"rankQuota"` // {"주임":n,...} annual point quota
|
||
Frozen bool `json:"frozen"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
}
|
||
|
||
func (m *IncentiveConfig) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||
|
||
// PaymentStage is a project-level stage bucket (one per kind×scope). The admin
|
||
// toggles Status as money arrives ("계약금 들어옴", "중도금까지 들어옴").
|
||
type PaymentStage struct {
|
||
Base
|
||
ProjectID string `gorm:"index" json:"projectId"`
|
||
Kind string `json:"kind"` // deposit | middle | final
|
||
Scope string `json:"scope"` // be | non_be
|
||
Amount float64 `json:"amount"` // KRW allocated to this bucket
|
||
Pct float64 `json:"pct"` // % of scope total
|
||
ExpectedDate string `json:"expectedDate"`
|
||
FixedDate string `json:"fixedDate"`
|
||
Status string `json:"status"` // planned | applying | applied | paid
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
}
|
||
|
||
func (m *PaymentStage) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||
|
||
// UserIncentive is the per (project, member, stage, scope) incentive record. It
|
||
// is derived from the member's portion but fully overridable per the spec
|
||
// ("특정 유저만 픽스하지 않는 상황" etc). Points = Amount × portion ÷ pointRate.
|
||
type UserIncentive struct {
|
||
Base
|
||
ProjectID string `gorm:"index" json:"projectId"`
|
||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||
StageID string `gorm:"index" json:"stageId"`
|
||
Kind string `json:"kind"`
|
||
Scope string `json:"scope"`
|
||
Year int `gorm:"index" json:"year"`
|
||
Quarter int `json:"quarter"` // 1..4 settlement bucket
|
||
Portion float64 `json:"portion"`
|
||
Amount float64 `json:"amount"` // KRW share
|
||
Points float64 `json:"points"`
|
||
FixStatus string `json:"fixStatus"` // planned | applying | applied | paid
|
||
Override bool `json:"override"` // manually adjusted (engine won't recompute)
|
||
Memo string `json:"memo"`
|
||
AppliedAt *time.Time `json:"appliedAt"`
|
||
PaidAt *time.Time `json:"paidAt"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
}
|
||
|
||
func (m *UserIncentive) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|
||
|
||
// QuarterlySettlement is the 3/6/9/12월 calculation snapshot per member: how much
|
||
// of their accumulated points exceed the rank quota, and the incremental payout.
|
||
type QuarterlySettlement struct {
|
||
Base
|
||
MemberEmail string `gorm:"index" json:"memberEmail"`
|
||
Year int `gorm:"index" json:"year"`
|
||
Quarter int `gorm:"index" json:"quarter"`
|
||
Rank string `json:"rank"`
|
||
Quota float64 `json:"quota"`
|
||
PointsCumul float64 `json:"pointsCumul"` // cumulative applied points to date
|
||
ExcessPoints float64 `json:"excessPoints"` // cumul − quota (floored at 0)
|
||
PaidPointsYTD float64 `json:"paidPointsYtd"` // already paid in prior quarters
|
||
PayoutPoints float64 `json:"payoutPoints"` // excess − paidYTD (this quarter's delta)
|
||
PayoutAmount float64 `json:"payoutAmount"` // KRW = payoutPoints × pointRate
|
||
Fixed bool `json:"fixed"`
|
||
FixedAt *time.Time `json:"fixedAt"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
}
|
||
|
||
func (m *QuarterlySettlement) BeforeCreate(*gorm.DB) error { m.ensureID(); return nil }
|