import { GraphQLError } from 'graphql' import { randomUUID, createHash } from 'crypto' import { prisma } from '../db.js' import { requireScopes, type AuthContext } from '../auth.js' import { materializeTeamFromContext } from '../team-materialization.js' export const teamTypeDefs = `#graphql type TeamUser { id: String firstName: String lastName: String phone: String avatarId: String createdAt: String } type TeamMember { user: TeamUser role: String! joinedAt: String } type TeamAddress { uuid: String! name: String! address: String! latitude: Float longitude: Float isDefault: Boolean! countryCode: String createdAt: String processedAt: String status: String! } type SelectedLocation { type: String uuid: String name: String latitude: Float longitude: Float } type Team { id: String! name: String! ownerId: String members: [TeamMember] addresses: [TeamAddress] selectedLocation: SelectedLocation } input InviteMemberInput { email: String! role: String } input CreateTeamAddressInput { name: String! address: String! latitude: Float longitude: Float countryCode: String isDefault: Boolean } input UpdateTeamAddressInput { uuid: String! name: String address: String latitude: Float longitude: Float countryCode: String isDefault: Boolean } input SetSelectedLocationInput { type: String! uuid: String! name: String! latitude: Float! longitude: Float! } type InviteMemberResult { success: Boolean!, message: String } type CreateTeamAddressResult { success: Boolean!, message: String, workflowId: String } type UpdateTeamAddressResult { success: Boolean!, address: TeamAddress } type DeleteTeamAddressResult { success: Boolean! } type SetSelectedLocationResult { success: Boolean! } type Query { team: Team getTeam(teamId: String!): Team teamMembers: [TeamMember] teamAddresses: [TeamAddress] } type Mutation { inviteMember(input: InviteMemberInput!): InviteMemberResult createTeamAddress(input: CreateTeamAddressInput!): CreateTeamAddressResult updateTeamAddress(input: UpdateTeamAddressInput!): UpdateTeamAddressResult deleteTeamAddress(uuid: String!): DeleteTeamAddressResult setSelectedLocation(input: SetSelectedLocationInput!): SetSelectedLocationResult } ` async function getTeamByUuid(uuid: string) { return prisma.team.findUnique({ where: { uuid }, include: { members: { include: { user: { include: { profile: true } } } }, addresses: { orderBy: { createdAt: 'desc' } }, }, }) } function mapTeam(team: NonNullable>>) { return { id: team.uuid, name: team.name, ownerId: team.ownerId?.toString() ?? null, members: team.members.map(m => ({ user: m.user ? { id: m.user.profile?.logtoId ?? m.user.id.toString(), firstName: m.user.firstName, lastName: m.user.lastName, phone: m.user.profile?.phone ?? '', avatarId: m.user.profile?.avatarId ?? null, createdAt: m.user.dateJoined.toISOString(), } : null, role: m.role, joinedAt: m.joinedAt.toISOString(), })), addresses: team.addresses.map(a => ({ uuid: a.uuid, name: a.name, address: a.address, latitude: a.latitude, longitude: a.longitude, isDefault: a.isDefault, countryCode: a.countryCode, createdAt: a.createdAt.toISOString(), processedAt: a.processedAt?.toISOString() ?? null, status: a.status, })), selectedLocation: team.selectedLocationType ? { type: team.selectedLocationType, uuid: team.selectedLocationUuid, name: team.selectedLocationName, latitude: team.selectedLocationLat, longitude: team.selectedLocationLon, } : null, } } export const teamResolvers = { Query: { team: async (_: unknown, __: unknown, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') if (!ctx.teamUuid) throw new GraphQLError('Team UUID not found') await materializeTeamFromContext(ctx) const team = await getTeamByUuid(ctx.teamUuid) return team ? mapTeam(team) : null }, getTeam: async (_: unknown, args: { teamId: string }, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') const team = await getTeamByUuid(args.teamId) return team ? mapTeam(team) : null }, teamMembers: async (_: unknown, __: unknown, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') if (!ctx.teamUuid) return [] await materializeTeamFromContext(ctx) const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } }) if (!team) return [] const members = await prisma.teamMember.findMany({ where: { teamId: team.id }, include: { user: { include: { profile: true } } }, }) return members.map(m => ({ user: m.user ? { id: m.user.profile?.logtoId ?? m.user.id.toString(), firstName: m.user.firstName, lastName: m.user.lastName, phone: m.user.profile?.phone ?? '', avatarId: m.user.profile?.avatarId ?? null, createdAt: m.user.dateJoined.toISOString(), } : null, role: m.role, joinedAt: m.joinedAt.toISOString(), })) }, teamAddresses: async (_: unknown, __: unknown, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') if (!ctx.teamUuid) return [] await materializeTeamFromContext(ctx) const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } }) if (!team) return [] const addrs = await prisma.teamAddress.findMany({ where: { teamId: team.id }, orderBy: { createdAt: 'desc' } }) return addrs.map(a => ({ uuid: a.uuid, name: a.name, address: a.address, latitude: a.latitude, longitude: a.longitude, isDefault: a.isDefault, countryCode: a.countryCode, createdAt: a.createdAt.toISOString(), processedAt: a.processedAt?.toISOString() ?? null, status: a.status, })) }, }, Mutation: { inviteMember: async (_: unknown, args: { input: { email: string; role?: string } }, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') if (!ctx.teamUuid || !ctx.userId) throw new GraphQLError('Not authenticated') await materializeTeamFromContext(ctx) const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } }) if (!team) throw new GraphQLError('Team not found') const invitation = await prisma.teamInvitation.create({ data: { teamId: team.id, email: args.input.email, role: args.input.role || 'MEMBER', invitedBy: ctx.userId, status: 'PENDING', }, }) const token = randomUUID() await prisma.teamInvitationToken.create({ data: { invitationId: invitation.id, tokenHash: createHash('sha256').update(token).digest('hex'), workflowStatus: 'active', }, }) return { success: true, message: 'Invitation created' } }, createTeamAddress: async (_: unknown, args: { input: { name: string; address: string; latitude?: number; longitude?: number; countryCode?: string; isDefault?: boolean } }, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') if (!ctx.teamUuid) throw new GraphQLError('Not authenticated') await materializeTeamFromContext(ctx) const team = await prisma.team.findUnique({ where: { uuid: ctx.teamUuid } }) if (!team) throw new GraphQLError('Team not found') const addr = await prisma.teamAddress.create({ data: { teamId: team.id, name: args.input.name, address: args.input.address, latitude: args.input.latitude, longitude: args.input.longitude, countryCode: args.input.countryCode || '', isDefault: args.input.isDefault || false, status: 'active', processedAt: new Date(), }, }) return { success: true, message: 'Address created', workflowId: null } }, updateTeamAddress: async (_: unknown, args: { input: { uuid: string; name?: string; address?: string; latitude?: number; longitude?: number; countryCode?: string; isDefault?: boolean } }, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') const data: Record = {} if (args.input.name !== undefined) data.name = args.input.name if (args.input.address !== undefined) data.address = args.input.address if (args.input.latitude !== undefined) data.latitude = args.input.latitude if (args.input.longitude !== undefined) data.longitude = args.input.longitude if (args.input.countryCode !== undefined) data.countryCode = args.input.countryCode if (args.input.isDefault !== undefined) data.isDefault = args.input.isDefault const addr = await prisma.teamAddress.update({ where: { uuid: args.input.uuid }, data }) return { success: true, address: { uuid: addr.uuid, name: addr.name, address: addr.address, latitude: addr.latitude, longitude: addr.longitude, isDefault: addr.isDefault, countryCode: addr.countryCode, createdAt: addr.createdAt.toISOString(), processedAt: addr.processedAt?.toISOString() ?? null, status: addr.status, }, } }, deleteTeamAddress: async (_: unknown, args: { uuid: string }, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') await prisma.teamAddress.delete({ where: { uuid: args.uuid } }) return { success: true } }, setSelectedLocation: async (_: unknown, args: { input: { type: string; uuid: string; name: string; latitude: number; longitude: number } }, ctx: AuthContext) => { requireScopes(ctx, 'teams:member') if (!ctx.teamUuid) throw new GraphQLError('Not authenticated') await materializeTeamFromContext(ctx) await prisma.team.update({ where: { uuid: ctx.teamUuid }, data: { selectedLocationType: args.input.type, selectedLocationUuid: args.input.uuid, selectedLocationName: args.input.name, selectedLocationLat: args.input.latitude, selectedLocationLon: args.input.longitude, }, }) return { success: true } }, }, }