Skip to content

Commit

Permalink
feat: add commission model to project (#754)
Browse files Browse the repository at this point in the history
  • Loading branch information
namnhce authored Oct 1, 2024
1 parent a24b0c3 commit 050b988
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 21 deletions.
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ require (
github.com/jackc/pgtype v1.14.0
github.com/jinzhu/now v1.1.5
github.com/joho/godotenv v1.5.1
github.com/k0kubun/pp v3.0.1+incompatible
github.com/k0kubun/pp/v3 v3.2.0
github.com/lib/pq v1.10.9
github.com/matoous/go-nanoid v1.5.0
github.com/patrickmn/go-cache v2.1.0+incompatible
Expand Down Expand Up @@ -129,7 +129,6 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40=
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs=
github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
Expand Down
3 changes: 2 additions & 1 deletion migrations/seed/permissions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,5 @@ INSERT INTO public.permissions (id, deleted_at, created_at, updated_at, name, co
('b4edad19-3e30-4e86-bd6e-07f535011aaf', null, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', 'Employee Discord Read', 'employees.discord.read'),
('1641106e-9a94-42fa-80e0-9d6978cd3596', null, '2024-05-29 15:41:12.475872', '2024-05-29 15:41:12.475872', 'Employee Discord Edit','employees.discord.edit'),
('b069e35c-1144-4554-9854-ff529506d4e5', null, '2024-05-29 15:41:12.475872', '2024-05-29 15:41:12.475872', 'Employee Discord Create','employees.discord.create'),
('f84e4e32-b104-4e9c-9694-b1a86e90ec25', null, '2024-09-19 15:41:12.475872', '2024-09-19 15:41:12.475872', 'Transfer Check-in Icy','employees.transferCheckinIcy.fullAccess');
('f84e4e32-b104-4e9c-9694-b1a86e90ec25', null, '2024-09-19 15:41:12.475872', '2024-09-19 15:41:12.475872', 'Transfer Check-in Icy','employees.transferCheckinIcy.fullAccess'),
('8fe4de41-15e8-4027-a769-e9344cd04415', null, '2024-09-19 15:41:12.475872', '2024-09-19 15:41:12.475872', 'Project Commission Models Read','projects.commissionModels.read');
6 changes: 4 additions & 2 deletions migrations/seed/role_permissions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ INSERT INTO public.role_permissions (id, deleted_at, created_at, updated_at, rol
('fdded785-6cc8-41e9-9fe4-5130b40584f9', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', '3fcf9e36-2501-4f86-8418-cfe3a137b7f9', 'b4edad19-3e30-4e86-bd6e-07f535011aaf'), -- employees.discord.read
('ae57f72a-1b48-4fdf-a76d-a0c796dc0943', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', '3fcf9e36-2501-4f86-8418-cfe3a137b7f9', '1641106e-9a94-42fa-80e0-9d6978cd3596'), -- employees.discord.edit
('81e3fd29-8bf4-408e-99db-42d27ca0b816', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', '3fcf9e36-2501-4f86-8418-cfe3a137b7f9', 'b069e35c-1144-4554-9854-ff529506d4e5'), -- employees.discord.create
('c5bb09ee-7f71-42e1-93b8-138cd2af2969', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', '3fcf9e36-2501-4f86-8418-cfe3a137b7f9', 'f84e4e32-b104-4e9c-9694-b1a86e90ec25'); -- employees.transferCheckinIcy.fullAccess
('c5bb09ee-7f71-42e1-93b8-138cd2af2969', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', '3fcf9e36-2501-4f86-8418-cfe3a137b7f9', 'f84e4e32-b104-4e9c-9694-b1a86e90ec25'), -- employees.transferCheckinIcy.fullAccess
('47826756-c795-473b-ab3c-f85b7ca3cf68', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', '3fcf9e36-2501-4f86-8418-cfe3a137b7f9', '8fe4de41-15e8-4027-a769-e9344cd04415');

-- FORTRESS VALUATION
INSERT INTO public.role_permissions (id, deleted_at, created_at, updated_at, role_id, permission_id) VALUES
Expand Down Expand Up @@ -282,4 +283,5 @@ INSERT INTO public.role_permissions (id, deleted_at, created_at, updated_at, rol
('bf4a2d78-da1c-447e-bd4e-a6f4a3997e4a', NULL, '2023-07-20 16:35:12.475872', '2023-07-20 16:35:12.475872', 'c23c1c1c-bfaf-41e6-a4d7-6ef196fd2736', '51a3ec4a-6bae-4f02-9d9b-89ba539346da'), -- deliveryMetrics.leaderBoard.read
('556e5273-630a-499c-8f69-0cda68c6ebda', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', 'c23c1c1c-bfaf-41e6-a4d7-6ef196fd2736', 'fea2497c-694d-43d7-82cd-764d622a6706'), -- deliveryMetrics.leaderBoard.sync
('27700990-ae6c-4a93-a9aa-5e9e71d8ac56', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', 'c23c1c1c-bfaf-41e6-a4d7-6ef196fd2736', '1991c441-39bc-4b0f-9afa-491bebb1c965'),
('67c5a96e-9e89-4e29-90f9-661b47e43c65', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', 'c23c1c1c-bfaf-41e6-a4d7-6ef196fd2736', 'f84e4e32-b104-4e9c-9694-b1a86e90ec25'); -- employees.transferCheckinIcy.fullAccess
('67c5a96e-9e89-4e29-90f9-661b47e43c65', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', 'c23c1c1c-bfaf-41e6-a4d7-6ef196fd2736', 'f84e4e32-b104-4e9c-9694-b1a86e90ec25'), -- employees.transferCheckinIcy.fullAccess
('b97c2450-63d5-4a6a-9f8d-9ac9541e0163', NULL, '2023-07-26 16:35:12.475872', '2023-07-26 16:35:12.475872', 'c23c1c1c-bfaf-41e6-a4d7-6ef196fd2736', '8fe4de41-15e8-4027-a769-e9344cd04415');
2 changes: 2 additions & 0 deletions pkg/handler/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/k0kubun/pp/v3"

"github.com/dwarvesf/fortress-api/pkg/config"
"github.com/dwarvesf/fortress-api/pkg/controller"
Expand Down Expand Up @@ -144,6 +145,7 @@ func (h *handler) CreateAPIKey(c *gin.Context) {
return
}

pp.Println("key created")
c.JSON(http.StatusOK, view.CreateResponse[any](&view.APIKeyData{
Key: key,
}, nil, nil, nil, ""))
Expand Down
5 changes: 5 additions & 0 deletions pkg/handler/project/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package project

import "github.com/gin-gonic/gin"

const (
saleReferralCommissionRate int64 = 10
)

type IHandler interface {
ArchiveWorkUnit(c *gin.Context)
AssignMember(c *gin.Context)
Expand All @@ -24,4 +28,5 @@ type IHandler interface {
UpdateWorkUnit(c *gin.Context)
UploadAvatar(c *gin.Context)
IcyWeeklyDistribution(c *gin.Context)
CommissionModels(c *gin.Context)
}
186 changes: 181 additions & 5 deletions pkg/handler/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"path/filepath"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -1659,7 +1660,7 @@ func (h *handler) Details(c *gin.Context) {
"id": projectID,
})

rs, err := h.store.Project.One(h.repo.DB(), projectID, true)
projectData, err := h.store.Project.One(h.repo.DB(), projectID, true)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
l.Info("project not found")
Expand All @@ -1672,19 +1673,135 @@ func (h *handler) Details(c *gin.Context) {
}

if !authutils.HasPermission(userInfo.Permissions, model.PermissionProjectsReadFullAccess) && !authutils.HasPermission(userInfo.Permissions, model.PermissionProjectsReadReadActive) {
_, ok := userInfo.Projects[rs.ID]
if !ok || !model.IsUserActiveInProject(userInfo.UserID, rs.ProjectMembers) {
_, ok := userInfo.Projects[projectData.ID]
if !ok || !model.IsUserActiveInProject(userInfo.UserID, projectData.ProjectMembers) {
c.JSON(http.StatusNotFound, view.CreateResponse[any](nil, nil, errs.ErrProjectNotFound, nil, ""))
return
}
}

if rs.Status == model.ProjectStatusClosed && !authutils.HasPermission(userInfo.Permissions, model.PermissionProjectsReadFullAccess) {
if projectData.Status == model.ProjectStatusClosed && !authutils.HasPermission(userInfo.Permissions, model.PermissionProjectsReadFullAccess) {
c.JSON(http.StatusNotFound, view.CreateResponse[any](nil, nil, errs.ErrProjectNotFound, nil, ""))
return
}

c.JSON(http.StatusOK, view.CreateResponse(view.ToProjectData(rs, userInfo), nil, nil, nil, ""))
projectCommissionModel := make([]model.CommissionModel, 0)
if authutils.HasPermission(userInfo.Permissions, model.PermissionProjectsCommissionRateRead) {
projectCommissionModel, err = h.aggregateCommissionModel(projectData)
if err != nil {
l.Error(err, "failed to aggregate commission model")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
return
}
}

c.JSON(http.StatusOK, view.CreateResponse(view.ToProjectData(projectData, userInfo, projectCommissionModel), nil, nil, nil, ""))
}

func (h *handler) aggregateCommissionModel(projectData *model.Project) ([]model.CommissionModel, error) {
employeeIDs := make([]model.UUID, 0)
var commissionModel = make([]model.CommissionModel, 0)
for _, h := range projectData.Heads {
commissionType := ""
description := ""
switch h.Position {
case model.HeadPositionTechnicalLead:
commissionType = "technical-lead"
description = "Technical Lead"
case model.HeadPositionAccountManager:
commissionType = "account-manager"
description = "Account Manager"
case model.HeadPositionDeliveryManager:
commissionType = "delivery-manager"
description = "Delivery Manager"
case model.HeadPositionSalePerson:
commissionType = "sale-person"
description = "Sale Person"
if !slices.Contains(employeeIDs, h.Employee.ReferredBy) {
employeeIDs = append(employeeIDs, h.Employee.ReferredBy)
}
}
if h.CommissionRate.IsZero() {
continue
}

commissionModel = append(commissionModel, model.CommissionModel{
Beneficiary: model.BasicEmployeeInfo{
ID: h.Employee.ID.String(),
FullName: h.Employee.FullName,
DisplayName: h.Employee.DisplayName,
Avatar: h.Employee.Avatar,
Username: h.Employee.Username,
ReferredBy: h.Employee.ReferredBy.String(),
},
CommissionType: commissionType,
CommissionRate: h.CommissionRate,
Description: description,
})
}
for _, pm := range projectData.ProjectMembers {
if pm.UpsellPerson != nil {
if pm.UpsellCommissionRate.IsZero() {
continue
}

commissionModel = append(commissionModel, model.CommissionModel{
Beneficiary: model.BasicEmployeeInfo{
ID: pm.UpsellPerson.ID.String(),
FullName: pm.UpsellPerson.FullName,
DisplayName: pm.UpsellPerson.DisplayName,
Avatar: pm.UpsellPerson.Avatar,
Username: pm.UpsellPerson.Username,
ReferredBy: pm.UpsellPerson.ReferredBy.String(),
},
CommissionType: "upsell",
CommissionRate: pm.UpsellCommissionRate,
Description: fmt.Sprintf("Upsell for %s", pm.Employee.FullName),
})

if !slices.Contains(employeeIDs, pm.UpsellPerson.ReferredBy) {
employeeIDs = append(employeeIDs, pm.UpsellPerson.ReferredBy)
}
}
}

refEmployees, err := h.store.Employee.GetByIDs(h.repo.DB(), employeeIDs)
if err != nil {
return nil, err
}

refEmployeeMap := make(map[string]model.Employee)
for _, ref := range refEmployees {
refEmployeeMap[ref.ID.String()] = *ref
}

finalCommissionModel := make([]model.CommissionModel, 0)
for _, cm := range commissionModel {
if cm.CommissionType == "upsell" || cm.CommissionType == "sale-person" {
if ref, ok := refEmployeeMap[cm.Beneficiary.ReferredBy]; ok {
description := fmt.Sprintf("Sale Referral from %s", cm.Beneficiary.FullName)
if cm.CommissionType == "upsell" {
description = fmt.Sprintf("Sale Referral (Upsell) from %s", cm.Beneficiary.FullName)
}
cm.Sub = &model.CommissionModel{
Beneficiary: model.BasicEmployeeInfo{
ID: ref.ID.String(),
FullName: ref.FullName,
DisplayName: ref.DisplayName,
Avatar: ref.Avatar,
Username: ref.Username,
ReferredBy: ref.ReferredBy.String(),
},
CommissionType: "sale-referral",
CommissionRate: decimal.NewFromInt(saleReferralCommissionRate),
Description: description,
}
}
}
finalCommissionModel = append(finalCommissionModel, cm)
}

return finalCommissionModel, nil
}

// UpdateGeneralInfo godoc
Expand Down Expand Up @@ -3149,3 +3266,62 @@ func (h *handler) SyncProjectMemberStatus(c *gin.Context) {

c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "ok"))
}

// CommissionModels godoc
// @Summary Get commission models of a project
// @Description Get commission models of a project
// @id getProjectCommissionModels
// @Tags Project
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Project ID"
// @Success 200 {object} ProjectCommissionModelsResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /projects/{id}/commission-models [get]
func (h *handler) CommissionModels(c *gin.Context) {
// 0. Get current logged in user data
userInfo, err := authutils.GetLoggedInUserInfo(c, h.store, h.repo.DB(), h.config)
if err != nil {
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, userInfo.UserID, ""))
return
}

projectID := c.Param("id")
if projectID == "" {
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, errs.ErrInvalidProjectID, nil, ""))
return
}

l := h.logger.Fields(logger.Fields{
"handler": "project",
"method": "CommissionModels",
"id": projectID,
})

projectData, err := h.store.Project.One(h.repo.DB(), projectID, true)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
l.Info("project not found")
c.JSON(http.StatusNotFound, view.CreateResponse[any](nil, nil, errs.ErrProjectNotFound, nil, ""))
return
}
l.Error(err, "error query project from db")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
return
}

projectCommissionModel := make([]model.CommissionModel, 0)
if authutils.HasPermission(userInfo.Permissions, model.PermissionProjectsCommissionRateRead) {
projectCommissionModel, err = h.aggregateCommissionModel(projectData)
if err != nil {
l.Error(err, "failed to aggregate commission model")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
return
}
}

c.JSON(http.StatusOK, view.CreateResponse(view.ToCommissionModelData(projectCommissionModel), nil, nil, nil, ""))
}
56 changes: 55 additions & 1 deletion pkg/handler/project/testdata/get_project/200.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,60 @@
}
],
"type": "dwarves",
"updatedAt": ""
"updatedAt": "",
"commissionModels": [
{
"beneficiary": {
"avatar": "https://s3-ap-southeast-1.amazonaws.com/fortress-images/5153574695663955944.png",
"displayName": "Thanh Pham",
"id": "2655832e-f009-4b73-a535-64c3a22e558f",
"fullName": "Phạm Đức Thành",
"username": "thanh"
},
"commissionRate": "1",
"description": "Account Manager",
"type": "account-manager",
"sub": null
},
{
"beneficiary": {
"avatar": "https://s3-ap-southeast-1.amazonaws.com/fortress-images/8399103964540935617.png",
"displayName": "Nam Nguyen",
"id": "8d7c99c0-3253-4286-93a9-e7554cb327ef",
"fullName": "Nguyễn Hải Nam",
"username": "benjamin"
},
"commissionRate": "1",
"description": "Technical Lead",
"type": "technical-lead",
"sub": null
},
{
"beneficiary": {
"avatar": "https://s3-ap-southeast-1.amazonaws.com/fortress-images/3c420751-bb9a-4878-896e-2f10f3a633d6_avatar2535921977139052349.png",
"displayName": "Huy Tieu",
"id": "608ea227-45a5-4c8a-af43-6c7280d96340",
"fullName": "Tiêu Quang Huy",
"username": "huytq"
},
"commissionRate": "2",
"description": "Sale Person",
"type": "sale-person",
"sub": null
},
{
"beneficiary": {
"avatar": "https://s3-ap-southeast-1.amazonaws.com/fortress-images/5153574695663955944.png",
"displayName": "Thanh Pham",
"id": "2655832e-f009-4b73-a535-64c3a22e558f",
"fullName": "Phạm Đức Thành",
"username": "thanh"
},
"commissionRate": "0.5",
"description": "Delivery Manager",
"type": "delivery-manager",
"sub": null
}
]
}
}
6 changes: 4 additions & 2 deletions pkg/handler/project/testdata/get_projects/200.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@
"symbol": "£",
"locale": "en-gb",
"type": "fiat"
}
},
"commissionModels": []
},
{
"id": "dfa182fc-1d2d-49f6-a877-c01da9ce4207",
Expand Down Expand Up @@ -292,7 +293,8 @@
"symbol": "£",
"locale": "en-gb",
"type": "fiat"
}
},
"commissionModels": []
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@
"symbol": "£",
"locale": "en-gb",
"type": "fiat"
}
},
"commissionModels": []
}
]
}
Loading

0 comments on commit 050b988

Please sign in to comment.