feat(groups): implement group creation functionality and refactor related components

- Removed organization dependency from group creation logic.
- Introduced new route and use-case for creating groups.
- Refactored form handling for group creation into a reusable GroupForm component.
- Updated API calls to support new group creation endpoint.
- Adjusted tests to reflect changes in group creation logic and validation.
- Removed obsolete organizations route and related components.
- Updated breadcrumb navigation and routing for group management.
This commit is contained in:
Jose Selesan
2026-09-23 09:19:52 -03:00
parent fc30927b8b
commit 5572ef0869
19 changed files with 456 additions and 644 deletions

View File

@@ -4,14 +4,13 @@ import { Hono } from 'hono';
const db = {
group: {
findMany: mock(),
findFirst: mock(),
count: mock(),
create: mock(),
},
groupMember: {
create: mock(),
},
organization: {
merchantAccount: {
findUnique: mock(),
},
};
@@ -102,19 +101,29 @@ describe('groups routes', () => {
expect(res.status).toBe(400);
});
it('creates a group from an owned organization', async () => {
const organization = {
id: 'org-1',
name: 'Escuela Alfa',
members: [{ userId, role: 'owner' }],
};
prisma.organization.findUnique.mockResolvedValue(organization as never);
prisma.group.findFirst.mockResolvedValue(null);
prisma.group.create.mockResolvedValue(group);
it('creates a group with billing configuration', async () => {
prisma.merchantAccount.findUnique.mockResolvedValue({ id: 'merchant-1', userId });
prisma.group.create.mockResolvedValue({
...group,
days: ['MONDAY'],
time: '09:00',
capacity: 20,
price: 500,
billingType: 'MONTHLY',
dueDay: 5,
});
const res = await makeApp({ id: userId }).request('/groups/from-organization', {
const res = await makeApp({ id: userId }).request('/groups', {
method: 'POST',
body: JSON.stringify({ organizationId: 'org-1' }),
body: JSON.stringify({
name: 'Cuadrilla Alfa',
days: ['MONDAY'],
time: '09:00',
capacity: 20,
price: 500,
billingType: 'MONTHLY',
dueDay: 5,
}),
headers: { 'Content-Type': 'application/json' },
});
@@ -122,77 +131,63 @@ describe('groups routes', () => {
expect(await res.json()).toEqual({
group: {
...group,
days: ['MONDAY'],
time: '09:00',
capacity: 20,
price: 500,
billingType: 'MONTHLY',
dueDay: 5,
createdAt: group.createdAt.toISOString(),
updatedAt: group.updatedAt.toISOString(),
},
alreadyExists: false,
});
expect(prisma.group.create).toHaveBeenCalledWith({
data: { name: 'Escuela Alfa', createdById: userId },
data: {
name: 'Cuadrilla Alfa',
createdById: userId,
days: ['MONDAY'],
time: '09:00',
capacity: 20,
price: 500,
billingType: 'MONTHLY',
dueDay: 5,
},
});
expect(prisma.groupMember.create).toHaveBeenCalledWith({
data: { groupId: 'group-1', userId, role: 'OWNER' },
});
});
it('reuses an existing group with the same name', async () => {
const organization = {
id: 'org-1',
name: 'Escuela Alfa',
members: [{ userId, role: 'owner' }],
};
prisma.organization.findUnique.mockResolvedValue(organization as never);
prisma.group.findFirst.mockResolvedValue(group);
it('rejects creating a group without a linked merchant account', async () => {
prisma.merchantAccount.findUnique.mockResolvedValue(null);
const res = await makeApp({ id: userId }).request('/groups/from-organization', {
const res = await makeApp({ id: userId }).request('/groups', {
method: 'POST',
body: JSON.stringify({ organizationId: 'org-1' }),
body: JSON.stringify({
name: 'Cuadrilla Alfa',
days: ['MONDAY'],
time: '09:00',
capacity: 20,
price: 500,
billingType: 'MONTHLY',
dueDay: 5,
}),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.alreadyExists).toBe(true);
expect(res.status).toBe(409);
expect(await res.json()).toMatchObject({ code: 'payment_not_setup' });
expect(prisma.group.create).not.toHaveBeenCalled();
});
it('rejects creating a group for an unknown organization', async () => {
prisma.organization.findUnique.mockResolvedValue(null);
const res = await makeApp({ id: userId }).request('/groups/from-organization', {
it('rejects creating a group with an invalid payload', async () => {
const res = await makeApp({ id: userId }).request('/groups', {
method: 'POST',
body: JSON.stringify({ organizationId: 'missing' }),
body: JSON.stringify({ name: 'x' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status).toBe(404);
expect(await res.json()).toMatchObject({ code: 'organization_not_found' });
expect(res.status).toBe(400);
expect(prisma.group.create).not.toHaveBeenCalled();
});
it('rejects creating a group when the user is not the owner', async () => {
const organization = {
id: 'org-1',
name: 'Escuela Alfa',
members: [{ userId: 'other-user', role: 'owner' }],
};
prisma.organization.findUnique.mockResolvedValue(organization as never);
const res = await makeApp({ id: userId }).request('/groups/from-organization', {
method: 'POST',
body: JSON.stringify({ organizationId: 'org-1' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status).toBe(403);
expect(await res.json()).toMatchObject({ code: 'group_owner_required' });
expect(prisma.group.create).not.toHaveBeenCalled();
});
it('rejects creating a group without a session', async () => {
const res = await makeApp(null).request('/groups/from-organization', {
method: 'POST',
body: JSON.stringify({ organizationId: 'org-1' }),
headers: { 'Content-Type': 'application/json' },
});
expect(res.status).toBe(401);
});
});