@open-mercato/core 0.6.3-develop.3810.1.ad92c339f5 → 0.6.3-develop.3820.1.636677865b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +37 -1
- package/dist/modules/customers/api/assignable-staff/route.js +14 -153
- package/dist/modules/customers/api/assignable-staff/route.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +1 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/planner/api/access.js +20 -53
- package/dist/modules/planner/api/access.js.map +2 -2
- package/dist/modules/staff/api/team-members/assignable/route.js +214 -0
- package/dist/modules/staff/api/team-members/assignable/route.js.map +7 -0
- package/dist/modules/staff/di.js +14 -0
- package/dist/modules/staff/di.js.map +7 -0
- package/dist/modules/staff/lib/availabilityAccess.js +73 -0
- package/dist/modules/staff/lib/availabilityAccess.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/AGENTS.md +27 -0
- package/src/modules/catalog/AGENTS.md +21 -2
- package/src/modules/currencies/AGENTS.md +21 -2
- package/src/modules/customer_accounts/AGENTS.md +26 -6
- package/src/modules/customers/AGENTS.md +20 -1
- package/src/modules/customers/api/assignable-staff/route.ts +14 -185
- package/src/modules/customers/components/detail/assignableStaff.ts +1 -1
- package/src/modules/data_sync/AGENTS.md +30 -11
- package/src/modules/integrations/AGENTS.md +31 -13
- package/src/modules/planner/api/access.ts +29 -55
- package/src/modules/progress/AGENTS.md +20 -2
- package/src/modules/sales/AGENTS.md +24 -5
- package/src/modules/staff/AGENTS.md +78 -0
- package/src/modules/staff/api/team-members/assignable/route.ts +257 -0
- package/src/modules/staff/di.ts +20 -0
- package/src/modules/staff/lib/availabilityAccess.ts +90 -0
- package/src/modules/workflows/AGENTS.md +25 -3
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
6
|
+
import { createPagedListResponseSchema as createSharedPagedListResponseSchema } from "@open-mercato/shared/lib/openapi/crud";
|
|
7
|
+
import { User } from "@open-mercato/core/modules/auth/data/entities";
|
|
8
|
+
import {
|
|
9
|
+
resolveAuthActorId,
|
|
10
|
+
resolveCustomersRequestContext
|
|
11
|
+
} from "@open-mercato/core/modules/customers/lib/interactionRequestContext";
|
|
12
|
+
import { StaffTeam, StaffTeamMember } from "../../../data/entities.js";
|
|
13
|
+
const querySchema = z.object({
|
|
14
|
+
page: z.coerce.number().min(1).default(1),
|
|
15
|
+
pageSize: z.coerce.number().min(1).max(100).default(24),
|
|
16
|
+
search: z.string().optional()
|
|
17
|
+
}).passthrough();
|
|
18
|
+
const itemSchema = z.object({
|
|
19
|
+
id: z.string().uuid(),
|
|
20
|
+
teamMemberId: z.string().uuid(),
|
|
21
|
+
userId: z.string().uuid(),
|
|
22
|
+
displayName: z.string(),
|
|
23
|
+
email: z.string().nullable().optional(),
|
|
24
|
+
teamName: z.string().nullable().optional(),
|
|
25
|
+
user: z.object({
|
|
26
|
+
id: z.string().uuid(),
|
|
27
|
+
email: z.string().nullable().optional()
|
|
28
|
+
}).nullable().optional(),
|
|
29
|
+
team: z.object({
|
|
30
|
+
id: z.string().uuid(),
|
|
31
|
+
name: z.string().nullable().optional()
|
|
32
|
+
}).nullable().optional()
|
|
33
|
+
});
|
|
34
|
+
const errorSchema = z.object({ error: z.string() });
|
|
35
|
+
const pagedListSchema = createSharedPagedListResponseSchema(itemSchema, {
|
|
36
|
+
paginationMetaOptional: true
|
|
37
|
+
});
|
|
38
|
+
const metadata = {
|
|
39
|
+
GET: { requireAuth: true, requireFeatures: ["customers.roles.view"] }
|
|
40
|
+
};
|
|
41
|
+
async function canAccessAssignableStaff(rbac, userId, scope) {
|
|
42
|
+
if (!rbac) return false;
|
|
43
|
+
if (await rbac.userHasAllFeatures(userId, ["customers.roles.manage"], scope)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return rbac.userHasAllFeatures(userId, ["customers.activities.manage"], scope);
|
|
47
|
+
}
|
|
48
|
+
async function GET(request) {
|
|
49
|
+
const { translate } = await resolveTranslations();
|
|
50
|
+
try {
|
|
51
|
+
const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams));
|
|
52
|
+
const { container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request);
|
|
53
|
+
if (!selectedOrganizationId) {
|
|
54
|
+
throw new CrudHttpError(
|
|
55
|
+
400,
|
|
56
|
+
{ error: translate("customers.errors.organization_required", "Organization context is required") }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const actorId = resolveAuthActorId(auth);
|
|
60
|
+
const rbacService = container.resolve("rbacService");
|
|
61
|
+
const scope = { tenantId: auth.tenantId, organizationId: selectedOrganizationId };
|
|
62
|
+
const hasAccess = await canAccessAssignableStaff(rbacService, actorId, scope);
|
|
63
|
+
if (!hasAccess) {
|
|
64
|
+
throw new CrudHttpError(
|
|
65
|
+
403,
|
|
66
|
+
{
|
|
67
|
+
error: translate(
|
|
68
|
+
"customers.assignableStaff.forbidden",
|
|
69
|
+
"Insufficient permissions to load assignable staff."
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const normalizedSearch = query.search?.trim().toLowerCase() ?? "";
|
|
75
|
+
const members = await findWithDecryption(
|
|
76
|
+
em,
|
|
77
|
+
StaffTeamMember,
|
|
78
|
+
{
|
|
79
|
+
tenantId: auth.tenantId,
|
|
80
|
+
organizationId: selectedOrganizationId,
|
|
81
|
+
deletedAt: null,
|
|
82
|
+
isActive: true
|
|
83
|
+
},
|
|
84
|
+
{ orderBy: { displayName: "asc" } },
|
|
85
|
+
scope
|
|
86
|
+
);
|
|
87
|
+
const userIds = Array.from(
|
|
88
|
+
new Set(
|
|
89
|
+
members.map((member) => typeof member.userId === "string" && member.userId.trim().length > 0 ? member.userId : null).filter((value) => typeof value === "string")
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
const teamIds = Array.from(
|
|
93
|
+
new Set(
|
|
94
|
+
members.map((member) => typeof member.teamId === "string" && member.teamId.trim().length > 0 ? member.teamId : null).filter((value) => typeof value === "string")
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
const [users, teams] = await Promise.all([
|
|
98
|
+
userIds.length > 0 ? findWithDecryption(
|
|
99
|
+
em,
|
|
100
|
+
User,
|
|
101
|
+
{
|
|
102
|
+
id: { $in: userIds },
|
|
103
|
+
deletedAt: null,
|
|
104
|
+
tenantId: auth.tenantId,
|
|
105
|
+
organizationId: selectedOrganizationId
|
|
106
|
+
},
|
|
107
|
+
void 0,
|
|
108
|
+
scope
|
|
109
|
+
) : Promise.resolve([]),
|
|
110
|
+
teamIds.length > 0 ? findWithDecryption(
|
|
111
|
+
em,
|
|
112
|
+
StaffTeam,
|
|
113
|
+
{
|
|
114
|
+
id: { $in: teamIds },
|
|
115
|
+
deletedAt: null,
|
|
116
|
+
tenantId: auth.tenantId,
|
|
117
|
+
organizationId: selectedOrganizationId
|
|
118
|
+
},
|
|
119
|
+
void 0,
|
|
120
|
+
scope
|
|
121
|
+
) : Promise.resolve([])
|
|
122
|
+
]);
|
|
123
|
+
const userById = new Map(
|
|
124
|
+
users.map((user) => [
|
|
125
|
+
user.id,
|
|
126
|
+
{
|
|
127
|
+
id: user.id,
|
|
128
|
+
email: user.email ?? null
|
|
129
|
+
}
|
|
130
|
+
])
|
|
131
|
+
);
|
|
132
|
+
const teamById = new Map(
|
|
133
|
+
teams.map((team) => [
|
|
134
|
+
team.id,
|
|
135
|
+
{
|
|
136
|
+
id: team.id,
|
|
137
|
+
name: team.name ?? null
|
|
138
|
+
}
|
|
139
|
+
])
|
|
140
|
+
);
|
|
141
|
+
const items = members.filter((member) => typeof member.userId === "string" && member.userId.trim().length > 0).map((member) => {
|
|
142
|
+
const userId = member.userId;
|
|
143
|
+
const user = userById.get(userId) ?? { id: userId, email: null };
|
|
144
|
+
const team = member.teamId ? teamById.get(member.teamId) ?? null : null;
|
|
145
|
+
return {
|
|
146
|
+
id: member.id,
|
|
147
|
+
teamMemberId: member.id,
|
|
148
|
+
userId,
|
|
149
|
+
displayName: member.displayName?.trim() || user.email || userId,
|
|
150
|
+
email: user.email,
|
|
151
|
+
teamName: team?.name ?? null,
|
|
152
|
+
user,
|
|
153
|
+
team
|
|
154
|
+
};
|
|
155
|
+
}).filter((item) => {
|
|
156
|
+
if (!normalizedSearch) return true;
|
|
157
|
+
const haystack = [item.displayName, item.email, item.teamName].filter((value) => typeof value === "string" && value.length > 0).join(" ").toLowerCase();
|
|
158
|
+
return haystack.includes(normalizedSearch);
|
|
159
|
+
});
|
|
160
|
+
const deduped = Array.from(
|
|
161
|
+
items.reduce((acc, item) => {
|
|
162
|
+
if (!acc.has(item.userId)) {
|
|
163
|
+
acc.set(item.userId, item);
|
|
164
|
+
}
|
|
165
|
+
return acc;
|
|
166
|
+
}, /* @__PURE__ */ new Map())
|
|
167
|
+
).map(([, item]) => item);
|
|
168
|
+
const start = (query.page - 1) * query.pageSize;
|
|
169
|
+
return NextResponse.json({
|
|
170
|
+
items: deduped.slice(start, start + query.pageSize),
|
|
171
|
+
total: deduped.length,
|
|
172
|
+
page: query.page,
|
|
173
|
+
pageSize: query.pageSize
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (isCrudHttpError(error)) {
|
|
177
|
+
return NextResponse.json(error.body, { status: error.status });
|
|
178
|
+
}
|
|
179
|
+
if (error instanceof z.ZodError) {
|
|
180
|
+
return NextResponse.json({ error: translate("customers.errors.validationFailed", "Validation failed") }, { status: 400 });
|
|
181
|
+
}
|
|
182
|
+
console.error("staff.assignable-team-members.get failed", error);
|
|
183
|
+
return NextResponse.json({ error: translate("customers.errors.assignable_staff_load_failed", "Failed to load assignable staff") }, { status: 500 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const openApi = {
|
|
187
|
+
tag: "Staff",
|
|
188
|
+
summary: "Assignable staff candidates",
|
|
189
|
+
methods: {
|
|
190
|
+
GET: {
|
|
191
|
+
summary: "List staff members that can be assigned from customer flows",
|
|
192
|
+
query: querySchema,
|
|
193
|
+
description: "Returns active staff members linked to auth users. Access requires either customers.roles.manage or customers.activities.manage. Owned by the staff module; consumed from customer flows via this canonical URL. Replaces the deprecated /api/customers/assignable-staff route.",
|
|
194
|
+
responses: [
|
|
195
|
+
{
|
|
196
|
+
status: 200,
|
|
197
|
+
description: "Assignable staff members",
|
|
198
|
+
schema: pagedListSchema
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
errors: [
|
|
202
|
+
{ status: 400, description: "Invalid request", schema: errorSchema },
|
|
203
|
+
{ status: 401, description: "Unauthorized", schema: errorSchema },
|
|
204
|
+
{ status: 403, description: "Forbidden", schema: errorSchema }
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
export {
|
|
210
|
+
GET,
|
|
211
|
+
metadata,
|
|
212
|
+
openApi
|
|
213
|
+
};
|
|
214
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/staff/api/team-members/assignable/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createPagedListResponseSchema as createSharedPagedListResponseSchema } from '@open-mercato/shared/lib/openapi/crud'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport {\n resolveAuthActorId,\n resolveCustomersRequestContext,\n} from '@open-mercato/core/modules/customers/lib/interactionRequestContext'\nimport { StaffTeam, StaffTeamMember } from '../../../data/entities'\n\nconst querySchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(24),\n search: z.string().optional(),\n })\n .passthrough()\n\nconst itemSchema = z.object({\n id: z.string().uuid(),\n teamMemberId: z.string().uuid(),\n userId: z.string().uuid(),\n displayName: z.string(),\n email: z.string().nullable().optional(),\n teamName: z.string().nullable().optional(),\n user: z\n .object({\n id: z.string().uuid(),\n email: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n team: z\n .object({\n id: z.string().uuid(),\n name: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n})\n\nconst errorSchema = z.object({ error: z.string() })\n\nconst pagedListSchema = createSharedPagedListResponseSchema(itemSchema, {\n paginationMetaOptional: true,\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.roles.view'] },\n}\n\nasync function canAccessAssignableStaff(\n rbac: RbacService | undefined,\n userId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<boolean> {\n if (!rbac) return false\n if (\n await rbac.userHasAllFeatures(userId, ['customers.roles.manage'], scope)\n ) {\n return true\n }\n return rbac.userHasAllFeatures(userId, ['customers.activities.manage'], scope)\n}\n\nexport async function GET(request: Request) {\n const { translate } = await resolveTranslations()\n try {\n const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams))\n const { container, em, auth, selectedOrganizationId } = await resolveCustomersRequestContext(request)\n\n if (!selectedOrganizationId) {\n throw new CrudHttpError(\n 400,\n { error: translate('customers.errors.organization_required', 'Organization context is required') },\n )\n }\n\n const actorId = resolveAuthActorId(auth)\n const rbacService = container.resolve('rbacService') as RbacService | undefined\n const scope = { tenantId: auth.tenantId, organizationId: selectedOrganizationId }\n const hasAccess = await canAccessAssignableStaff(rbacService, actorId, scope)\n if (!hasAccess) {\n throw new CrudHttpError(\n 403,\n {\n error: translate(\n 'customers.assignableStaff.forbidden',\n 'Insufficient permissions to load assignable staff.',\n ),\n },\n )\n }\n\n const normalizedSearch = query.search?.trim().toLowerCase() ?? ''\n\n const members = await findWithDecryption(\n em,\n StaffTeamMember,\n {\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n deletedAt: null,\n isActive: true,\n },\n { orderBy: { displayName: 'asc' } },\n scope,\n )\n\n const userIds = Array.from(\n new Set(\n members\n .map((member) => (typeof member.userId === 'string' && member.userId.trim().length > 0 ? member.userId : null))\n .filter((value): value is string => typeof value === 'string'),\n ),\n )\n const teamIds = Array.from(\n new Set(\n members\n .map((member) => (typeof member.teamId === 'string' && member.teamId.trim().length > 0 ? member.teamId : null))\n .filter((value): value is string => typeof value === 'string'),\n ),\n )\n\n const [users, teams] = await Promise.all([\n userIds.length > 0\n ? findWithDecryption(\n em,\n User,\n {\n id: { $in: userIds },\n deletedAt: null,\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n },\n undefined,\n scope,\n )\n : Promise.resolve([]),\n teamIds.length > 0\n ? findWithDecryption(\n em,\n StaffTeam,\n {\n id: { $in: teamIds },\n deletedAt: null,\n tenantId: auth.tenantId,\n organizationId: selectedOrganizationId,\n },\n undefined,\n scope,\n )\n : Promise.resolve([]),\n ])\n\n const userById = new Map(\n users.map((user) => [\n user.id,\n {\n id: user.id,\n email: user.email ?? null,\n },\n ]),\n )\n const teamById = new Map(\n teams.map((team) => [\n team.id,\n {\n id: team.id,\n name: team.name ?? null,\n },\n ]),\n )\n\n const items = members\n .filter((member) => typeof member.userId === 'string' && member.userId.trim().length > 0)\n .map((member) => {\n const userId = member.userId as string\n const user = userById.get(userId) ?? { id: userId, email: null }\n const team = member.teamId ? teamById.get(member.teamId) ?? null : null\n return {\n id: member.id,\n teamMemberId: member.id,\n userId,\n displayName: member.displayName?.trim() || user.email || userId,\n email: user.email,\n teamName: team?.name ?? null,\n user,\n team,\n }\n })\n .filter((item) => {\n if (!normalizedSearch) return true\n const haystack = [item.displayName, item.email, item.teamName]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .join(' ')\n .toLowerCase()\n return haystack.includes(normalizedSearch)\n })\n\n const deduped = Array.from(\n items.reduce((acc, item) => {\n if (!acc.has(item.userId)) {\n acc.set(item.userId, item)\n }\n return acc\n }, new Map<string, (typeof items)[number]>()),\n ).map(([, item]) => item)\n\n const start = (query.page - 1) * query.pageSize\n return NextResponse.json({\n items: deduped.slice(start, start + query.pageSize),\n total: deduped.length,\n page: query.page,\n pageSize: query.pageSize,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n if (error instanceof z.ZodError) {\n return NextResponse.json({ error: translate('customers.errors.validationFailed', 'Validation failed') }, { status: 400 })\n }\n console.error('staff.assignable-team-members.get failed', error)\n return NextResponse.json({ error: translate('customers.errors.assignable_staff_load_failed', 'Failed to load assignable staff') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Staff',\n summary: 'Assignable staff candidates',\n methods: {\n GET: {\n summary: 'List staff members that can be assigned from customer flows',\n query: querySchema,\n description:\n 'Returns active staff members linked to auth users. Access requires either customers.roles.manage or customers.activities.manage. Owned by the staff module; consumed from customer flows via this canonical URL. Replaces the deprecated /api/customers/assignable-staff route.',\n responses: [\n {\n status: 200,\n description: 'Assignable staff members',\n schema: pagedListSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request', schema: errorSchema },\n { status: 401, description: 'Unauthorized', schema: errorSchema },\n { status: 403, description: 'Forbidden', schema: errorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { asValue } from "awilix";
|
|
2
|
+
import {
|
|
3
|
+
resolveAvailabilityWriteAccess
|
|
4
|
+
} from "./lib/availabilityAccess.js";
|
|
5
|
+
function register(container) {
|
|
6
|
+
const resolver = { resolveAvailabilityWriteAccess };
|
|
7
|
+
container.register({
|
|
8
|
+
availabilityAccessResolver: asValue(resolver)
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
register
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=di.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/staff/di.ts"],
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n resolveAvailabilityWriteAccess,\n type AvailabilityAccessContext,\n type AvailabilityWriteAccess,\n} from './lib/availabilityAccess'\n\nexport type AvailabilityAccessResolver = {\n resolveAvailabilityWriteAccess(\n ctx: AvailabilityAccessContext,\n ): Promise<AvailabilityWriteAccess>\n}\n\nexport function register(container: AppContainer) {\n const resolver: AvailabilityAccessResolver = { resolveAvailabilityWriteAccess }\n container.register({\n availabilityAccessResolver: asValue(resolver),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,OAGK;AAQA,SAAS,SAAS,WAAyB;AAChD,QAAM,WAAuC,EAAE,+BAA+B;AAC9E,YAAU,SAAS;AAAA,IACjB,4BAA4B,QAAQ,QAAQ;AAAA,EAC9C,CAAC;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { StaffTeamMember } from "../data/entities.js";
|
|
3
|
+
const MANAGE_AVAILABILITY_FEATURE = "planner.manage_availability";
|
|
4
|
+
const SELF_MANAGE_FEATURE = "staff.my_availability.manage";
|
|
5
|
+
const SELF_UNAVAILABILITY_FEATURE = "staff.my_availability.unavailability";
|
|
6
|
+
async function resolveAvailabilityWriteAccess(ctx) {
|
|
7
|
+
const auth = ctx.auth;
|
|
8
|
+
const tenantId = auth?.tenantId ?? null;
|
|
9
|
+
const organizationId = ctx.selectedOrganizationId ?? auth?.orgId ?? null;
|
|
10
|
+
if (!auth || !auth.sub || auth.isApiKey) {
|
|
11
|
+
return {
|
|
12
|
+
canManageAll: false,
|
|
13
|
+
canManageSelf: false,
|
|
14
|
+
canManageUnavailability: false,
|
|
15
|
+
memberId: null,
|
|
16
|
+
tenantId,
|
|
17
|
+
organizationId
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const rbac = ctx.container.resolve("rbacService");
|
|
21
|
+
const canManageAll = await rbac.userHasAllFeatures(
|
|
22
|
+
auth.sub,
|
|
23
|
+
[MANAGE_AVAILABILITY_FEATURE],
|
|
24
|
+
{ tenantId, organizationId }
|
|
25
|
+
);
|
|
26
|
+
if (canManageAll) {
|
|
27
|
+
return {
|
|
28
|
+
canManageAll: true,
|
|
29
|
+
canManageSelf: true,
|
|
30
|
+
canManageUnavailability: true,
|
|
31
|
+
memberId: null,
|
|
32
|
+
tenantId,
|
|
33
|
+
organizationId
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const [canManageSelf, canManageUnavailability] = await Promise.all([
|
|
37
|
+
rbac.userHasAllFeatures(auth.sub, [SELF_MANAGE_FEATURE], { tenantId, organizationId }),
|
|
38
|
+
rbac.userHasAllFeatures(auth.sub, [SELF_UNAVAILABILITY_FEATURE], { tenantId, organizationId })
|
|
39
|
+
]);
|
|
40
|
+
if (!canManageSelf) {
|
|
41
|
+
return {
|
|
42
|
+
canManageAll: false,
|
|
43
|
+
canManageSelf: false,
|
|
44
|
+
canManageUnavailability: false,
|
|
45
|
+
memberId: null,
|
|
46
|
+
tenantId,
|
|
47
|
+
organizationId
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const em = ctx.container.resolve("em");
|
|
51
|
+
const member = await findOneWithDecryption(
|
|
52
|
+
em,
|
|
53
|
+
StaffTeamMember,
|
|
54
|
+
{ userId: auth.sub, deletedAt: null },
|
|
55
|
+
void 0,
|
|
56
|
+
{ tenantId, organizationId }
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
canManageAll: false,
|
|
60
|
+
canManageSelf,
|
|
61
|
+
canManageUnavailability,
|
|
62
|
+
memberId: member?.id ?? null,
|
|
63
|
+
tenantId,
|
|
64
|
+
organizationId
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
MANAGE_AVAILABILITY_FEATURE,
|
|
69
|
+
SELF_MANAGE_FEATURE,
|
|
70
|
+
SELF_UNAVAILABILITY_FEATURE,
|
|
71
|
+
resolveAvailabilityWriteAccess
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=availabilityAccess.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/staff/lib/availabilityAccess.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { StaffTeamMember } from '../data/entities'\n\nexport type AvailabilityAccessContext = {\n container: AwilixContainer\n auth: AuthContext | null\n selectedOrganizationId?: string | null\n}\n\nexport type AvailabilityWriteAccess = {\n canManageAll: boolean\n canManageSelf: boolean\n canManageUnavailability: boolean\n memberId: string | null\n tenantId: string | null\n organizationId: string | null\n unregistered?: boolean\n}\n\nexport const MANAGE_AVAILABILITY_FEATURE = 'planner.manage_availability'\nexport const SELF_MANAGE_FEATURE = 'staff.my_availability.manage'\nexport const SELF_UNAVAILABILITY_FEATURE = 'staff.my_availability.unavailability'\n\nexport async function resolveAvailabilityWriteAccess(\n ctx: AvailabilityAccessContext,\n): Promise<AvailabilityWriteAccess> {\n const auth = ctx.auth\n const tenantId = auth?.tenantId ?? null\n const organizationId = ctx.selectedOrganizationId ?? auth?.orgId ?? null\n if (!auth || !auth.sub || auth.isApiKey) {\n return {\n canManageAll: false,\n canManageSelf: false,\n canManageUnavailability: false,\n memberId: null,\n tenantId,\n organizationId,\n }\n }\n const rbac = ctx.container.resolve('rbacService') as RbacService\n const canManageAll = await rbac.userHasAllFeatures(\n auth.sub,\n [MANAGE_AVAILABILITY_FEATURE],\n { tenantId, organizationId },\n )\n if (canManageAll) {\n return {\n canManageAll: true,\n canManageSelf: true,\n canManageUnavailability: true,\n memberId: null,\n tenantId,\n organizationId,\n }\n }\n const [canManageSelf, canManageUnavailability] = await Promise.all([\n rbac.userHasAllFeatures(auth.sub, [SELF_MANAGE_FEATURE], { tenantId, organizationId }),\n rbac.userHasAllFeatures(auth.sub, [SELF_UNAVAILABILITY_FEATURE], { tenantId, organizationId }),\n ])\n if (!canManageSelf) {\n return {\n canManageAll: false,\n canManageSelf: false,\n canManageUnavailability: false,\n memberId: null,\n tenantId,\n organizationId,\n }\n }\n const em = ctx.container.resolve('em') as EntityManager\n const member = await findOneWithDecryption(\n em,\n StaffTeamMember,\n { userId: auth.sub, deletedAt: null },\n undefined,\n { tenantId, organizationId },\n )\n return {\n canManageAll: false,\n canManageSelf,\n canManageUnavailability,\n memberId: member?.id ?? null,\n tenantId,\n organizationId,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAkBzB,MAAM,8BAA8B;AACpC,MAAM,sBAAsB;AAC5B,MAAM,8BAA8B;AAE3C,eAAsB,+BACpB,KACkC;AAClC,QAAM,OAAO,IAAI;AACjB,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,iBAAiB,IAAI,0BAA0B,MAAM,SAAS;AACpE,MAAI,CAAC,QAAQ,CAAC,KAAK,OAAO,KAAK,UAAU;AACvC,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,IAAI,UAAU,QAAQ,aAAa;AAChD,QAAM,eAAe,MAAM,KAAK;AAAA,IAC9B,KAAK;AAAA,IACL,CAAC,2BAA2B;AAAA,IAC5B,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,MAAI,cAAc;AAChB,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,CAAC,eAAe,uBAAuB,IAAI,MAAM,QAAQ,IAAI;AAAA,IACjE,KAAK,mBAAmB,KAAK,KAAK,CAAC,mBAAmB,GAAG,EAAE,UAAU,eAAe,CAAC;AAAA,IACrF,KAAK,mBAAmB,KAAK,KAAK,CAAC,2BAA2B,GAAG,EAAE,UAAU,eAAe,CAAC;AAAA,EAC/F,CAAC;AACD,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,MACL,cAAc;AAAA,MACd,eAAe;AAAA,MACf,yBAAyB;AAAA,MACzB,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,KAAK,IAAI,UAAU,QAAQ,IAAI;AACrC,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,IACA,EAAE,QAAQ,KAAK,KAAK,WAAW,KAAK;AAAA,IACpC;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,SAAO;AAAA,IACL,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA,UAAU,QAAQ,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3820.1.636677865b",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.3-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.3-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.3-develop.3820.1.636677865b",
|
|
247
|
+
"@open-mercato/shared": "0.6.3-develop.3820.1.636677865b",
|
|
248
|
+
"@open-mercato/ui": "0.6.3-develop.3820.1.636677865b",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.3-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.3-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.3-develop.3820.1.636677865b",
|
|
254
|
+
"@open-mercato/shared": "0.6.3-develop.3820.1.636677865b",
|
|
255
|
+
"@open-mercato/ui": "0.6.3-develop.3820.1.636677865b",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
The auth module handles authentication, authorization, users, roles, and RBAC.
|
|
4
4
|
|
|
5
|
+
## Always
|
|
6
|
+
|
|
7
|
+
1. Hash passwords with `bcryptjs` using cost >= 10.
|
|
8
|
+
2. Use `findWithDecryption` / `findOneWithDecryption` for user queries.
|
|
9
|
+
3. Prefer `requireFeatures` in page/API metadata for access control.
|
|
10
|
+
4. Declare every module feature in `acl.ts` and seed role grants through `setup.ts`.
|
|
11
|
+
5. Use wildcard-aware helpers such as `matchFeature`, `hasFeature`, and `hasAllFeatures` when inspecting raw granted features.
|
|
12
|
+
|
|
13
|
+
## Ask First
|
|
14
|
+
|
|
15
|
+
- Ask before changing session token format, RBAC semantics, wildcard matching, super-admin behavior, or tenant provisioning outputs.
|
|
16
|
+
- Ask before changing login/reset/invitation error messages because auth messages can leak account existence.
|
|
17
|
+
|
|
18
|
+
## Never
|
|
19
|
+
|
|
20
|
+
- Never log credentials, password reset tokens, session tokens, or decrypted user secrets.
|
|
21
|
+
- Never reveal whether an email exists through auth error messages.
|
|
22
|
+
- Never check raw ACL arrays with `includes(...)`, `Set.has(...)`, or ad hoc wildcard logic.
|
|
23
|
+
|
|
24
|
+
## Validation Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn generate
|
|
28
|
+
yarn workspace @open-mercato/core build
|
|
29
|
+
yarn workspace @open-mercato/core test
|
|
30
|
+
```
|
|
31
|
+
|
|
5
32
|
## Data Model
|
|
6
33
|
|
|
7
34
|
- **Users** — system users with credentials, profile, preferences
|
|
@@ -2,14 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Use the catalog module for products, categories, pricing, variants, and offers.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Always
|
|
6
6
|
|
|
7
|
-
1. **MUST
|
|
7
|
+
1. **MUST use `selectBestPrice` and the resolver pipeline from `lib/pricing.ts`** for pricing logic.
|
|
8
8
|
2. **MUST use the DI token `catalogPricingService`** when resolving prices — ensures overrides take effect
|
|
9
9
|
3. **MUST register custom pricing resolvers** with explicit priority (`registerCatalogPricingResolver(resolver, { priority })`)
|
|
10
10
|
4. **MUST declare widget injections** in `widgets/injection/` and map via `injection-table.ts`
|
|
11
11
|
5. **MUST follow the standard event pattern** in `events.ts` for all CRUD and lifecycle events
|
|
12
12
|
|
|
13
|
+
## Ask First
|
|
14
|
+
|
|
15
|
+
- Ask before changing price-layer precedence, resolver priority semantics, event IDs, or released widget spot IDs.
|
|
16
|
+
- Ask before deleting or changing option schemas that existing variants may reference.
|
|
17
|
+
|
|
18
|
+
## Never
|
|
19
|
+
|
|
20
|
+
- Never reimplement catalog pricing inline.
|
|
21
|
+
- Never delete option schemas while variants reference them.
|
|
22
|
+
- Never bypass `prepareMutation` for catalog AI tools that mutate products, prices, or media.
|
|
23
|
+
|
|
24
|
+
## Validation Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn db:generate
|
|
28
|
+
yarn generate
|
|
29
|
+
yarn workspace @open-mercato/core build
|
|
30
|
+
```
|
|
31
|
+
|
|
13
32
|
## When You Need Pricing Logic
|
|
14
33
|
|
|
15
34
|
1. Resolve `catalogPricingService` from DI
|
|
@@ -2,13 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Use the currencies module for multi-currency support, exchange rates, and currency conversion.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Always
|
|
6
6
|
|
|
7
7
|
1. **MUST store currency amounts with 4 decimal precision** — never truncate to 2 decimals internally
|
|
8
8
|
2. **MUST use date-based exchange rates** — always resolve rates for the transaction date, not "current" rate
|
|
9
9
|
3. **MUST record both transaction currency and base currency amounts** — dual recording is mandatory for reporting
|
|
10
10
|
4. **MUST calculate realized gains/losses** on payment: `(payment rate - invoice rate) × foreign amount`
|
|
11
|
-
5. **MUST
|
|
11
|
+
5. **MUST keep financial postings atomic** — full transaction rollback on error
|
|
12
|
+
|
|
13
|
+
## Ask First
|
|
14
|
+
|
|
15
|
+
- Ask before changing precision, exchange-rate lookup semantics, realized gain/loss formulas, or financial reporting fields.
|
|
16
|
+
- Ask before changing historical exchange-rate retention behavior.
|
|
17
|
+
|
|
18
|
+
## Never
|
|
19
|
+
|
|
20
|
+
- Never truncate internal currency amounts to 2 decimals.
|
|
21
|
+
- Never hard-delete exchange rate records — rates are historical reference data.
|
|
22
|
+
- Never delete posted transactions or audit-trail entries.
|
|
23
|
+
|
|
24
|
+
## Validation Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn db:generate
|
|
28
|
+
yarn generate
|
|
29
|
+
yarn workspace @open-mercato/core build
|
|
30
|
+
```
|
|
12
31
|
|
|
13
32
|
## Key Files
|
|
14
33
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Customer-facing identity and portal authentication with a two-tier RBAC model. This module manages customer user accounts, sessions, roles, invitations, and the authentication flow for the customer portal. It is separate from the internal `auth` module, which handles staff authentication.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Always
|
|
6
6
|
|
|
7
7
|
1. **MUST hash passwords with `bcryptjs` (cost >= 10)** — never store plaintext passwords
|
|
8
8
|
2. **MUST return minimal error messages on auth endpoints** — never reveal whether an email exists (use generic "Invalid email or password")
|
|
@@ -10,11 +10,31 @@ Customer-facing identity and portal authentication with a two-tier RBAC model. T
|
|
|
10
10
|
4. **MUST validate all inputs with zod** — schemas live in `data/validators.ts`
|
|
11
11
|
5. **MUST export `openApi`** from every API route file
|
|
12
12
|
6. **MUST scope all queries by `tenantId`** and filter `deletedAt: null` for soft-deleted records
|
|
13
|
-
7. **MUST
|
|
14
|
-
8. **MUST use `
|
|
15
|
-
9. **MUST
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
7. **MUST use `hashForLookup` for email-based lookups** — emails are stored with a deterministic hash for indexed queries
|
|
14
|
+
8. **MUST use `hashToken` for storing session/verification/reset tokens** — raw tokens are never persisted
|
|
15
|
+
9. **MUST emit events via `emitCustomerAccountsEvent`** for all state changes (login, signup, lock, password reset)
|
|
16
|
+
|
|
17
|
+
## Ask First
|
|
18
|
+
|
|
19
|
+
- Ask before changing cookie names, token TTLs, JWT claim shape, rate limits, lockout thresholds, or portal RBAC semantics.
|
|
20
|
+
- Ask before moving customer portal navigation into a different staff IA group.
|
|
21
|
+
- Ask before changing CRM auto-linking behavior.
|
|
22
|
+
|
|
23
|
+
## Never
|
|
24
|
+
|
|
25
|
+
- Never store plaintext passwords or raw tokens.
|
|
26
|
+
- Never reveal whether an email exists.
|
|
27
|
+
- Never expose cross-tenant data — session validation checks tenant match.
|
|
28
|
+
- Never import staff auth services; customer auth is a fully separate identity system.
|
|
29
|
+
- Never use exact `includes(...)` checks for portal wildcard ACL matching.
|
|
30
|
+
|
|
31
|
+
## Validation Commands
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
yarn db:generate
|
|
35
|
+
yarn generate
|
|
36
|
+
yarn workspace @open-mercato/core build
|
|
37
|
+
```
|
|
18
38
|
|
|
19
39
|
## Data Model
|
|
20
40
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**This is the reference CRUD module.** When building new modules, copy patterns from here first.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Always
|
|
6
6
|
|
|
7
7
|
1. **MUST use this module as the template** for new CRUD modules — copy file structure and patterns
|
|
8
8
|
2. **MUST include all standard module files** — use the list below as a checklist
|
|
@@ -11,6 +11,25 @@
|
|
|
11
11
|
5. **MUST capture custom field snapshots** in command `before`/`after` payloads for undo support
|
|
12
12
|
6. **MUST use `useGuardedMutation` for non-`CrudForm` backend writes** (`POST`/`PUT`/`PATCH`/`DELETE`) and pass `retryLastMutation` in injection context
|
|
13
13
|
|
|
14
|
+
## Ask First
|
|
15
|
+
|
|
16
|
+
- Ask before changing this module's reference patterns, standard module-file checklist, or AI mutation policies.
|
|
17
|
+
- Ask before changing customer data model relationships that downstream modules may copy.
|
|
18
|
+
|
|
19
|
+
## Never
|
|
20
|
+
|
|
21
|
+
- Never bypass custom field normalization in CRUD create/update/read responses.
|
|
22
|
+
- Never omit undo snapshots for custom field mutations.
|
|
23
|
+
- Never write backend `POST`/`PUT`/`PATCH`/`DELETE` actions outside `CrudForm` without `useGuardedMutation`.
|
|
24
|
+
|
|
25
|
+
## Validation Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn db:generate
|
|
29
|
+
yarn generate
|
|
30
|
+
yarn workspace @open-mercato/core build
|
|
31
|
+
```
|
|
32
|
+
|
|
14
33
|
## Key Reference Files — Copy From Here
|
|
15
34
|
|
|
16
35
|
| When you need | Copy from |
|