Skip to content

feat: Add Valorant roles/description to contest application and member list (#73)#74

Merged
Sunja-An merged 3 commits intodevelopfrom
feat/#73
Mar 23, 2026
Merged

feat: Add Valorant roles/description to contest application and member list (#73)#74
Sunja-An merged 3 commits intodevelopfrom
feat/#73

Conversation

@Sunja-An
Copy link
Copy Markdown
Contributor

Overview

Closes #73

When applying to a Valorant contest, applicants can now select their in-game role(s) and write a short self-introduction. This information is also exposed in the contest member list API so that contest leaders can review participants' profiles at a glance.


Changes

Domain

  • Defined ValorantRole type with 4 constants: DUELIST, INITIATOR, CONTROLLER, SENTINEL
  • Implemented ValorantRoles as a custom JSON-serializable slice type (implements driver.Valuer / sql.Scanner) to persist cleanly in MySQL without external libraries
  • Added ValorantRoles and Description fields to ContestMember entity

Application Layer

  • Extended SenderSnapshot (Redis) and ContestMemberWithUser (DB port) with the new fields
  • Added RequestParticipateRequest DTO with optional valorant_roles and description
  • Validation in service layer:
    • description must not exceed 64 characters
    • Each ValorantRole value must be one of the 4 valid roles
    • Duplicate roles in a single request are rejected
  • AcceptApplication now copies roles and description from the Redis snapshot into the persisted ContestMember

Presentation Layer

  • POST /api/contests/:id/applications now accepts an optional JSON request body
  • Existing clients that send no body continue to work without change

Infrastructure

  • GetMembersWithUserByContest query updated to SELECT the two new columns
  • ContestMemberResponse DTO extended; ToContestMemberResponse updated accordingly

Error Codes

Code Constant Trigger
CT035 ErrDescriptionTooLong description > 64 chars
CT036 ErrInvalidValorantRole unknown role value
CT037 ErrDuplicateValorantRole same role submitted twice

Database Migration

000025_add_valorant_fields_to_contests_members

ALTER TABLE contests_members
    ADD COLUMN valorant_roles JSON DEFAULT NULL AFTER point,
    ADD COLUMN description VARCHAR(64) DEFAULT '' AFTER valorant_roles;

Both columns are nullable / default-safe so existing rows are unaffected. A DOWN migration is provided for rollback.


Result

POST /api/contests/:id/applications

Request body (optional)

{
  "valorant_roles": ["DUELIST", "SENTINEL"],
  "description": "I can flex any role."
}

Behavior

  • Body omitted → roles and description stored as zero values (backward compatible)
  • game_type != VALORANT → roles and description are accepted but carry no game-specific meaning (no hard block, keeps the API generic)

GET /api/contests/:id/members

Response (added fields)

{
  "user_id": 1,
  "username": "gamer",
  "member_type": "NORMAL",
  "leader_type": "MEMBER",
  "point": 1200,
  "valorant_roles": ["DUELIST", "SENTINEL"],
  "description": "I can flex any role.",
  ...
}

Sunja-An and others added 2 commits February 9, 2026 02:29
…r list (#73)

- Define ValorantRole type (DUELIST, INITIATOR, CONTROLLER, SENTINEL) in domain
- Add ValorantRoles (custom JSON type) and Description to ContestMember entity
- Extend SenderSnapshot and ContestMemberWithUser with new fields
- Accept optional request body in POST /api/contests/:id/applications
- Validate description max 64 chars and role deduplication in service layer
- Include valorant_roles and description in GET /api/contests/:id/members response
- Add DB migration 000025 to add valorant_roles JSON and description VARCHAR(64) columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the contest application process by allowing users to specify their Valorant in-game roles and provide a short self-introduction. This information is then stored and made accessible through the contest member list API, providing contest leaders with valuable insights into participants' profiles. The changes involve updates across the domain, application, presentation, and infrastructure layers, including new data types, validation rules, API modifications, and database schema updates.

Highlights

  • New Data Fields for Contest Applications: Introduced ValorantRole and ValorantRoles types to represent in-game roles, including custom JSON serialization for database persistence, and added a Description field for self-introductions.
  • Extended Entities and DTOs: Extended the ContestMember entity, Redis SenderSnapshot, and various DTOs (RequestParticipateRequest, SenderResponse, ContestMemberWithUser, ContestMemberResponse) to incorporate Valorant roles and the new description field.
  • Application Validation: Implemented validation logic for contest applications, ensuring description length limits (max 64 characters) and the validity and uniqueness of submitted Valorant roles.
  • API Endpoint Updates: Updated the contest application API (POST /api/contests/:id/applications) to optionally accept Valorant roles and descriptions, maintaining backward compatibility. The contest member list API (GET /api/contests/:id/members) was also modified to expose these new fields.
  • Database Migration: Provided database migrations to add valorant_roles (JSON) and description (VARCHAR(64)) columns to the contests_members table, with both up and down migration scripts.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a feature to allow applicants of Valorant contests to specify their roles and a self-introduction. The changes are well-structured, touching the database, domain, application, and presentation layers. My review focuses on improving the validation logic to provide more specific error feedback, enhancing error handling in the controller, and ensuring consistency in error codes and constants.

// Parse optional request body
var req dto.RequestParticipateRequest
// ShouldBindJSON returns error if body exists but is invalid; nil body is OK
_ = ctx.ShouldBindJSON(&req)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The error returned by ctx.ShouldBindJSON(&req) is being ignored. While an empty request body is acceptable, a malformed JSON body will cause ShouldBindJSON to return an error. By ignoring it, the server will proceed as if no body was sent, which is confusing for API clients who provided a body and expected it to be processed. The error should be handled to return a 400 Bad Request for invalid JSON. Note that ShouldBindJSON returns io.EOF for an empty body, which should be explicitly ignored.

Suggested change
_ = ctx.ShouldBindJSON(&req)
if err := ctx.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
response.JSON(ctx, response.BadRequest("invalid request body: "+err.Error()))
return
}

func (s *ContestApplicationService) RequestParticipate(ctx context.Context, contestId, userId int64, req *dto.RequestParticipateRequest) (*dto.DiscordLinkRequiredResponse, error) {
// Validate request if provided
if req != nil {
if len(req.Description) > 64 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The value 64 is a magic number representing the maximum length of the description. This value is also hardcoded in the database migration (VARCHAR(64)). To improve maintainability and ensure consistency, it's best practice to define this as a constant (e.g., domain.MaxDescriptionLength) and reuse it across the application.

Comment on lines +58 to +63
if len(req.ValorantRoles) > 0 {
roles := domain.ValorantRoles(req.ValorantRoles)
if !roles.AreValid() {
return nil, exception.ErrInvalidValorantRole
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This validation logic returns a generic ErrInvalidValorantRole for any issue with the roles. To provide more specific feedback, the AreValid method in the domain should return a distinct error for duplicate roles (as ErrDuplicateValorantRole has been defined). This requires updating the AreValid method to return an error and then handling that error here.

Suggested change
if len(req.ValorantRoles) > 0 {
roles := domain.ValorantRoles(req.ValorantRoles)
if !roles.AreValid() {
return nil, exception.ErrInvalidValorantRole
}
}
if len(req.ValorantRoles) > 0 {
roles := domain.ValorantRoles(req.ValorantRoles)
if err := roles.AreValid(); err != nil {
return nil, err
}
}

Comment on lines +67 to +79
func (vr ValorantRoles) AreValid() bool {
seen := make(map[ValorantRole]bool)
for _, role := range vr {
if !role.IsValid() {
return false
}
if seen[role] {
return false
}
seen[role] = true
}
return true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The AreValid method currently returns a bool, which prevents the caller from distinguishing between an invalid role and a duplicate role. A specific error, ErrDuplicateValorantRole, has been defined but is not used because of this. To provide more specific feedback to the user, this method should be changed to return an error. This will allow the service layer to return the correct error code. Additionally, using map[ValorantRole]struct{} is more idiomatic in Go for implementing a set.

Suggested change
func (vr ValorantRoles) AreValid() bool {
seen := make(map[ValorantRole]bool)
for _, role := range vr {
if !role.IsValid() {
return false
}
if seen[role] {
return false
}
seen[role] = true
}
return true
}
func (vr ValorantRoles) AreValid() error {
seen := make(map[ValorantRole]struct{})
for _, role := range vr {
if !role.IsValid() {
return exception.ErrInvalidValorantRole
}
if _, exists := seen[role]; exists {
return exception.ErrDuplicateValorantRole
}
seen[role] = struct{}{}
}
return nil
}

Comment on lines +41 to +43
ErrInvalidValorantRole = NewBadRequestError("invalid valorant role", "CT035")
ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT036")
ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is an inconsistency in the error codes between this file and the PR description. The description states CT035 for ErrDescriptionTooLong and CT036 for ErrInvalidValorantRole, but the codes are swapped here. To maintain consistency and avoid confusion for API consumers, the codes should be corrected to match the documentation.

Suggested change
ErrInvalidValorantRole = NewBadRequestError("invalid valorant role", "CT035")
ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT036")
ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037")
ErrInvalidValorantRole = NewBadRequestError("invalid valorant role", "CT036")
ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT035")
ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037")

@Sunja-An Sunja-An changed the base branch from main to develop March 23, 2026 07:23
@Sunja-An Sunja-An merged commit d6826e6 into develop Mar 23, 2026
@Sunja-An Sunja-An deleted the feat/#73 branch March 23, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add Valorant roles/description to contest application and member list

1 participant