Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add commission model to project #754

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading