workflow_version: "v003" feature: "add label management system" created_at: "2025-12-18T17:45:00" updated_at: "2025-12-18T17:50:00" approved_at: null status: draft revision: 2 revision_notes: "Updated to remove external model references (existing models in codebase)" # Note: This design extends existing models (Label, Artist, Song, Album, User) # which are already defined in prisma/schema.prisma # The dependencies on these models are implicit (referenced by foreign key field names) # ============================================================================ # LAYER 1: DATA MODELS # ============================================================================ data_models: - id: model_label_invitation name: LabelInvitation description: "Invitations from labels to artists to join their roster" table_name: label_invitations fields: - name: id type: uuid constraints: [primary_key] description: "Unique identifier" - name: labelId type: uuid constraints: [foreign_key, not_null, indexed] description: "Reference to the inviting label (FK to labels table)" - name: artistId type: uuid constraints: [foreign_key, not_null, indexed] description: "Reference to the invited artist (FK to artists table)" - name: status type: enum enum_values: [pending, accepted, declined, expired] constraints: [not_null, default] default: pending description: "Current status of the invitation" - name: message type: text constraints: [] description: "Optional message from label to artist" - name: expiresAt type: datetime constraints: [not_null] description: "When the invitation expires" - name: createdAt type: datetime constraints: [not_null] description: "When invitation was created" - name: updatedAt type: datetime constraints: [not_null] description: "When invitation was last updated" relations: [] # Relations are to existing models, handled by FK fields indexes: - fields: [labelId, artistId] unique: true name: label_artist_unique - fields: [artistId, status] unique: false name: artist_pending_invitations timestamps: true soft_delete: false validations: - field: expiresAt rule: "future_date" message: "Expiration date must be in the future" # ============================================================================ # LAYER 2: API ENDPOINTS # ============================================================================ api_endpoints: # === LABEL CRUD === - id: api_get_label method: GET path: /api/labels/[id] summary: "Get label details" description: "Retrieve label profile with artist roster and statistics" tags: [labels] path_params: - name: id type: string description: "Label ID" responses: - status: 200 description: "Label found" schema: type: object properties: - name: id type: uuid - name: name type: string - name: slug type: string - name: description type: string - name: logoUrl type: string - name: website type: string - name: artists type: array - name: _count type: object example: id: "550e8400-e29b-41d4-a716-446655440000" name: "Sonic Records" slug: "sonic-records" description: "Independent music label" artists: [] _count: { artists: 5, songs: 120 } - status: 404 description: "Label not found" schema: type: object properties: - name: error type: string depends_on_models: [model_label_invitation] # New model only depends_on_apis: [] auth: required: false roles: [] - id: api_update_label method: PUT path: /api/labels/[id] summary: "Update label profile" description: "Update label name, description, logo, and website" tags: [labels] path_params: - name: id type: string description: "Label ID" request_body: content_type: application/json schema: type: object properties: - name: name type: string required: false description: "Label name" - name: description type: string required: false description: "Label description" - name: logoUrl type: string required: false description: "Logo URL" - name: website type: string required: false description: "Website URL" responses: - status: 200 description: "Label updated" schema: type: object properties: - name: id type: uuid - name: name type: string - name: slug type: string - name: description type: string - status: 401 description: "Unauthorized" - status: 403 description: "Not label owner" - status: 404 description: "Label not found" depends_on_models: [] depends_on_apis: [] auth: required: true roles: [label] # === LABEL STATISTICS === - id: api_get_label_stats method: GET path: /api/labels/[id]/stats summary: "Get label statistics" description: "Get artist count, song count, album count, and total plays" tags: [labels, statistics] path_params: - name: id type: string description: "Label ID" responses: - status: 200 description: "Statistics retrieved" schema: type: object properties: - name: artistCount type: integer - name: songCount type: integer - name: albumCount type: integer - name: totalPlays type: integer example: artistCount: 5 songCount: 120 albumCount: 15 totalPlays: 45000 - status: 404 description: "Label not found" depends_on_models: [] depends_on_apis: [] auth: required: false roles: [] # === LABEL INVITATIONS === - id: api_list_label_invitations method: GET path: /api/labels/[id]/invitations summary: "List label invitations" description: "Get all invitations sent by this label" tags: [labels, invitations] path_params: - name: id type: string description: "Label ID" query_params: - name: status type: string required: false description: "Filter by status (pending, accepted, declined, expired)" responses: - status: 200 description: "Invitations list" schema: type: array properties: - name: id type: uuid - name: artist type: object - name: status type: string - name: message type: string - name: expiresAt type: datetime - status: 401 description: "Unauthorized" - status: 403 description: "Not label owner" depends_on_models: [model_label_invitation] depends_on_apis: [] auth: required: true roles: [label] - id: api_create_label_invitation method: POST path: /api/labels/[id]/invitations summary: "Create artist invitation" description: "Send invitation to an artist to join the label" tags: [labels, invitations] path_params: - name: id type: string description: "Label ID" request_body: content_type: application/json schema: type: object properties: - name: artistId type: string required: true description: "ID of artist to invite" - name: message type: string required: false description: "Optional invitation message" responses: - status: 201 description: "Invitation created" schema: type: object properties: - name: id type: uuid - name: artistId type: uuid - name: status type: string - name: expiresAt type: datetime - status: 400 description: "Artist already has label or pending invitation" - status: 401 description: "Unauthorized" - status: 403 description: "Not label owner" - status: 404 description: "Artist not found" depends_on_models: [model_label_invitation] depends_on_apis: [] auth: required: true roles: [label] - id: api_delete_label_invitation method: DELETE path: /api/labels/[id]/invitations/[invitationId] summary: "Cancel invitation" description: "Cancel a pending invitation" tags: [labels, invitations] path_params: - name: id type: string description: "Label ID" - name: invitationId type: string description: "Invitation ID" responses: - status: 200 description: "Invitation cancelled" - status: 401 description: "Unauthorized" - status: 403 description: "Not label owner" - status: 404 description: "Invitation not found" depends_on_models: [model_label_invitation] depends_on_apis: [] auth: required: true roles: [label] # === ARTIST INVITATION HANDLING === - id: api_list_artist_invitations method: GET path: /api/artists/[id]/invitations summary: "List artist invitations" description: "Get all invitations received by this artist" tags: [artists, invitations] path_params: - name: id type: string description: "Artist ID" responses: - status: 200 description: "Invitations list" schema: type: array properties: - name: id type: uuid - name: label type: object - name: status type: string - name: message type: string - name: expiresAt type: datetime - status: 401 description: "Unauthorized" - status: 403 description: "Not the artist" depends_on_models: [model_label_invitation] depends_on_apis: [] auth: required: true roles: [artist] - id: api_respond_to_invitation method: POST path: /api/artists/[id]/invitations/[invitationId]/respond summary: "Respond to invitation" description: "Accept or decline a label invitation" tags: [artists, invitations] path_params: - name: id type: string description: "Artist ID" - name: invitationId type: string description: "Invitation ID" request_body: content_type: application/json schema: type: object properties: - name: response type: string required: true description: "Accept or decline (accept/decline)" responses: - status: 200 description: "Response recorded" schema: type: object properties: - name: status type: string - name: label type: object - status: 400 description: "Invalid response or invitation already processed" - status: 401 description: "Unauthorized" - status: 403 description: "Not the invited artist" - status: 404 description: "Invitation not found" depends_on_models: [model_label_invitation] depends_on_apis: [] auth: required: true roles: [artist] # === ARTIST MANAGEMENT === - id: api_remove_artist_from_label method: DELETE path: /api/labels/[id]/artists/[artistId] summary: "Remove artist from label" description: "Remove an artist from the label roster" tags: [labels, artists] path_params: - name: id type: string description: "Label ID" - name: artistId type: string description: "Artist ID" responses: - status: 200 description: "Artist removed from label" - status: 401 description: "Unauthorized" - status: 403 description: "Not label owner" - status: 404 description: "Artist not in label" depends_on_models: [] depends_on_apis: [] auth: required: true roles: [label] # ============================================================================ # LAYER 3: UI PAGES # ============================================================================ pages: - id: page_label_profile name: "Label Profile" path: /label/[id] layout: layout_main data_needs: - api_id: api_get_label purpose: "Display label info and artist roster" on_load: true - api_id: api_get_label_stats purpose: "Display label statistics" on_load: true components: - component_label_header - component_label_stats - component_artist_roster seo: title: "{{label.name}} | Sonic Cloud" description: "{{label.description}}" auth: required: false roles: [] redirect: null - id: page_label_dashboard name: "Label Dashboard" path: /label/dashboard layout: layout_main data_needs: - api_id: api_get_label purpose: "Display label info" on_load: true - api_id: api_get_label_stats purpose: "Display statistics" on_load: true - api_id: api_list_label_invitations purpose: "Show pending invitations" on_load: true components: - component_label_stats - component_artist_roster - component_invitation_card - component_invite_artist_modal seo: title: "Label Dashboard | Sonic Cloud" description: "Manage your label" auth: required: true roles: [label] redirect: /login - id: page_label_settings name: "Label Settings" path: /label/settings layout: layout_main data_needs: - api_id: api_get_label purpose: "Load current label data for editing" on_load: true components: - component_label_profile_form seo: title: "Label Settings | Sonic Cloud" description: "Edit your label profile" auth: required: true roles: [label] redirect: /login - id: page_label_invitations name: "Label Invitations" path: /label/invitations layout: layout_main data_needs: - api_id: api_list_label_invitations purpose: "List all invitations" on_load: true components: - component_invitation_card - component_invite_artist_modal seo: title: "Invitations | Sonic Cloud" description: "Manage artist invitations" auth: required: true roles: [label] redirect: /login # ============================================================================ # LAYER 3: UI COMPONENTS # ============================================================================ components: - id: component_label_card name: LabelCard props: - name: label type: "Label" required: true description: "Label object to display" - name: showArtistCount type: boolean required: false default: true description: "Show number of artists" events: - name: onClick payload: "string" description: "Fires when card clicked, payload is label ID" uses_apis: [] uses_components: [] internal_state: [] variants: [default, compact] - id: component_label_header name: LabelHeader props: - name: label type: "Label" required: true description: "Label with full details" - name: isOwner type: boolean required: false default: false description: "Is current user the label owner" events: - name: onEdit payload: "void" description: "Fires when edit button clicked" uses_apis: [] uses_components: [] internal_state: [] variants: [default] - id: component_label_stats name: LabelStats props: - name: stats type: "LabelStats" required: true description: "Statistics object" events: [] uses_apis: [] uses_components: [] internal_state: [] variants: [default, compact] - id: component_artist_roster name: ArtistRoster props: - name: artists type: "Artist[]" required: true description: "List of signed artists" - name: isOwner type: boolean required: false default: false description: "Show management controls" - name: emptyMessage type: string required: false default: "No artists signed yet" description: "Message when roster is empty" events: - name: onRemoveArtist payload: "string" description: "Fires when remove clicked, payload is artist ID" - name: onArtistClick payload: "string" description: "Fires when artist clicked" uses_apis: [] uses_components: [] # Uses existing ArtistCard component internal_state: [removingArtistId] variants: [grid, list] - id: component_invitation_card name: InvitationCard props: - name: invitation type: "LabelInvitation" required: true description: "Invitation object" - name: viewType type: string required: true description: "Are we viewing as label or artist (label/artist)" events: - name: onAccept payload: "string" description: "Fires when accept clicked (artist view)" - name: onDecline payload: "string" description: "Fires when decline clicked (artist view)" - name: onCancel payload: "string" description: "Fires when cancel clicked (label view)" uses_apis: [] uses_components: [] internal_state: [isProcessing] variants: [default] - id: component_invite_artist_modal name: InviteArtistModal props: - name: isOpen type: boolean required: true description: "Whether modal is open" - name: labelId type: string required: true description: "Label ID sending invitation" events: - name: onClose payload: "void" description: "Fires when modal should close" - name: onInviteSent payload: "LabelInvitation" description: "Fires when invitation successfully sent" uses_apis: [api_create_label_invitation] uses_components: [] internal_state: [searchQuery, selectedArtist, message, isSubmitting] variants: [default] - id: component_label_profile_form name: LabelProfileForm props: - name: label type: "Label" required: true description: "Current label data" events: - name: onSave payload: "Label" description: "Fires when form saved successfully" - name: onCancel payload: "void" description: "Fires when cancel clicked" uses_apis: [api_update_label] uses_components: [] internal_state: [formData, isSubmitting, errors] variants: [default]