fix: support product tag push and optional register job number
This commit is contained in:
parent
007dd0a986
commit
b829efbaab
@ -164,6 +164,39 @@ describe('POST /api/auth/register', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('工号为空时创建待激活普通账号并等待管理员手动设置权限', async () => {
|
||||
mockCreateUser.mockResolvedValue({
|
||||
id: 'user-external',
|
||||
email: 'external@example.com',
|
||||
name: '外部录入',
|
||||
role: 'USER',
|
||||
businessRoles: [],
|
||||
employeeDirectoryId: null,
|
||||
})
|
||||
|
||||
const response = await POST(createRequest({
|
||||
email: 'external@example.com',
|
||||
name: '外部录入',
|
||||
jobNumber: '',
|
||||
password: 'abc12345',
|
||||
}))
|
||||
|
||||
expect(mockMatchByJobNumber).not.toHaveBeenCalled()
|
||||
expect(mockFindByEmployeeIdentity).not.toHaveBeenCalled()
|
||||
expect(mockResolveDepartmentIdByExternalId).not.toHaveBeenCalled()
|
||||
expect(mockCreateUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: 'external@example.com',
|
||||
name: '外部录入',
|
||||
role: 'USER',
|
||||
businessRoles: [],
|
||||
status: 'PENDING',
|
||||
isFirstLogin: false,
|
||||
}),
|
||||
)
|
||||
expect(response).toEqual(expect.objectContaining({ success: true, status: 201 }))
|
||||
})
|
||||
|
||||
it('姓名与员工库不一致时拒绝注册,不绑定员工身份', async () => {
|
||||
mockMatchByJobNumber.mockResolvedValue({
|
||||
id: 'emp-1',
|
||||
|
||||
@ -21,7 +21,7 @@ interface RegisterRequest {
|
||||
email: string
|
||||
name: string
|
||||
password: string
|
||||
jobNumber: string
|
||||
jobNumber?: string
|
||||
// role 参数被忽略,注册只允许创建 USER 角色
|
||||
departmentId?: string
|
||||
teamId?: string
|
||||
@ -40,66 +40,83 @@ export async function POST(request: NextRequest) {
|
||||
request,
|
||||
async (ctx) => {
|
||||
const body: RegisterRequest = await request.json()
|
||||
const { email, name, password, jobNumber } = body
|
||||
|
||||
const employee = await externalEmployeeDirectoryService.matchByJobNumber(jobNumber)
|
||||
if (!employee || employee.sourceStatus !== 'EMPLOYED') {
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_FAILED,
|
||||
GENERIC_REGISTER_REVIEW_MESSAGE
|
||||
)
|
||||
}
|
||||
const { email, name, password } = body
|
||||
const jobNumber = typeof body.jobNumber === 'string' ? body.jobNumber.trim() : ''
|
||||
|
||||
const submittedName = normalizePersonName(name)
|
||||
const employeeName = normalizePersonName(employee.name)
|
||||
if (submittedName !== employeeName) {
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_FAILED,
|
||||
GENERIC_REGISTER_REVIEW_MESSAGE
|
||||
let canAutoActivate = false
|
||||
let user: Awaited<ReturnType<typeof userService.create>>
|
||||
|
||||
if (jobNumber) {
|
||||
const employee = await externalEmployeeDirectoryService.matchByJobNumber(jobNumber)
|
||||
if (!employee || employee.sourceStatus !== 'EMPLOYED') {
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_FAILED,
|
||||
GENERIC_REGISTER_REVIEW_MESSAGE
|
||||
)
|
||||
}
|
||||
|
||||
const employeeName = normalizePersonName(employee.name)
|
||||
if (submittedName !== employeeName) {
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_FAILED,
|
||||
GENERIC_REGISTER_REVIEW_MESSAGE
|
||||
)
|
||||
}
|
||||
|
||||
const existingEmployeeUser = await userService.findByEmployeeIdentity({
|
||||
employeeDirectoryId: employee.id,
|
||||
dingtalkUserId: employee.dingtalkUserId,
|
||||
dingtalkUnionId: employee.dingtalkUnionId,
|
||||
})
|
||||
if (existingEmployeeUser) {
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_FAILED,
|
||||
GENERIC_REGISTER_REVIEW_MESSAGE
|
||||
)
|
||||
}
|
||||
|
||||
const departmentId = await externalEmployeeDirectoryService.resolveDepartmentIdByExternalId(
|
||||
employee.primaryDepartmentExternalId
|
||||
)
|
||||
const department = departmentId
|
||||
? await departmentService.findById(departmentId).catch(() => null)
|
||||
: null
|
||||
const provisioning = resolveEmployeeAccountProvisioning({
|
||||
sourceStatus: employee.sourceStatus,
|
||||
departmentFullPath: department?.fullPath,
|
||||
})
|
||||
canAutoActivate = provisioning.canAutoActivate
|
||||
|
||||
// 调用服务层创建用户
|
||||
// 强制使用 USER 角色,忽略用户传入的 role 参数,防止权限提升攻击
|
||||
user = await userService.create({
|
||||
email,
|
||||
name: employeeName,
|
||||
password,
|
||||
role: REGISTER_DEFAULT_ROLE,
|
||||
businessRoles: provisioning.businessRoles,
|
||||
status: provisioning.status,
|
||||
departmentId: departmentId ?? undefined,
|
||||
isFirstLogin: false,
|
||||
phone: employee.mobile ?? undefined,
|
||||
employeeDirectoryId: employee.id,
|
||||
dingtalkUserId: employee.dingtalkUserId,
|
||||
dingtalkUnionId: employee.dingtalkUnionId,
|
||||
dingtalkBoundAt: employee.dingtalkUserId || employee.dingtalkUnionId ? new Date() : null,
|
||||
})
|
||||
} else {
|
||||
user = await userService.create({
|
||||
email,
|
||||
name: submittedName,
|
||||
password,
|
||||
role: REGISTER_DEFAULT_ROLE,
|
||||
businessRoles: [],
|
||||
status: 'PENDING',
|
||||
isFirstLogin: false,
|
||||
})
|
||||
}
|
||||
|
||||
const existingEmployeeUser = await userService.findByEmployeeIdentity({
|
||||
employeeDirectoryId: employee.id,
|
||||
dingtalkUserId: employee.dingtalkUserId,
|
||||
dingtalkUnionId: employee.dingtalkUnionId,
|
||||
})
|
||||
if (existingEmployeeUser) {
|
||||
throw new AppError(
|
||||
ErrorCode.VALIDATION_FAILED,
|
||||
GENERIC_REGISTER_REVIEW_MESSAGE
|
||||
)
|
||||
}
|
||||
|
||||
const departmentId = await externalEmployeeDirectoryService.resolveDepartmentIdByExternalId(
|
||||
employee.primaryDepartmentExternalId
|
||||
)
|
||||
const department = departmentId
|
||||
? await departmentService.findById(departmentId).catch(() => null)
|
||||
: null
|
||||
const provisioning = resolveEmployeeAccountProvisioning({
|
||||
sourceStatus: employee.sourceStatus,
|
||||
departmentFullPath: department?.fullPath,
|
||||
})
|
||||
|
||||
// 调用服务层创建用户
|
||||
// 强制使用 USER 角色,忽略用户传入的 role 参数,防止权限提升攻击
|
||||
const user = await userService.create({
|
||||
email,
|
||||
name: employeeName,
|
||||
password,
|
||||
role: REGISTER_DEFAULT_ROLE,
|
||||
businessRoles: provisioning.businessRoles,
|
||||
status: provisioning.status,
|
||||
departmentId: departmentId ?? undefined,
|
||||
isFirstLogin: false,
|
||||
phone: employee.mobile ?? undefined,
|
||||
employeeDirectoryId: employee.id,
|
||||
dingtalkUserId: employee.dingtalkUserId,
|
||||
dingtalkUnionId: employee.dingtalkUnionId,
|
||||
dingtalkBoundAt: employee.dingtalkUserId || employee.dingtalkUnionId ? new Date() : null,
|
||||
})
|
||||
|
||||
// 记录操作日志
|
||||
await logService.create({
|
||||
userId: user.id,
|
||||
@ -115,7 +132,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return jsonSuccess(
|
||||
{
|
||||
message: provisioning.canAutoActivate ? '注册成功,账号已自动激活' : '注册成功,请等待管理员激活',
|
||||
message: canAutoActivate ? '注册成功,账号已自动激活' : '注册成功,请等待管理员激活',
|
||||
user,
|
||||
},
|
||||
201,
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
createEmptyNonIntegratedPatentApplication,
|
||||
createEmptyNonIntegratedProjectReview,
|
||||
NON_INTEGRATED_DEFAULT_BRAND,
|
||||
NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
|
||||
NON_INTEGRATED_PRODUCTS_UPDATED,
|
||||
type NonIntegratedSkuComponentDetail,
|
||||
@ -754,6 +755,7 @@ function buildDraftGroupSeriesProductRecord({
|
||||
sourceSeriesCode: draftGroup.assetSeriesCode?.trim().toUpperCase() || '',
|
||||
source: 'draft_group_modular_series',
|
||||
},
|
||||
productTag: NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
spu: normalizedSeriesSpu,
|
||||
projectStatus: 'approved',
|
||||
projectReview: createEmptyNonIntegratedProjectReview(),
|
||||
@ -3071,6 +3073,9 @@ function updateFormalProductSkuItems(
|
||||
const batchKey = String(batchNo)
|
||||
return {
|
||||
...product,
|
||||
productTag: Object.prototype.hasOwnProperty.call(product, 'productTag')
|
||||
? product.productTag ?? ''
|
||||
: NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
skuItems,
|
||||
skuBatchCount: Math.max(product.skuBatchCount ?? 1, batchNo),
|
||||
skuBatchFlowNos: {
|
||||
|
||||
@ -264,6 +264,7 @@ import {
|
||||
NON_INTEGRATED_DUPLICATE_SPU_MESSAGE,
|
||||
NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES,
|
||||
NON_INTEGRATED_PLANNING_LAYOUT_WORKSPACE_CATEGORY,
|
||||
NON_INTEGRATED_PRODUCT_TAG_OPTIONS,
|
||||
NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES,
|
||||
getNonIntegratedFormalSpu,
|
||||
getNonIntegratedProductById,
|
||||
@ -6650,6 +6651,7 @@ function buildProductPayload(
|
||||
createdBy?: string,
|
||||
updatedBy?: string,
|
||||
productDevelopmentKind: ProductDevelopmentKind = "product",
|
||||
productTag = "",
|
||||
metadata?: Record<string, unknown>,
|
||||
): NonIntegratedProductRecord {
|
||||
const now = new Date().toISOString();
|
||||
@ -6901,6 +6903,7 @@ function buildProductPayload(
|
||||
id,
|
||||
productDevelopmentKind,
|
||||
...(metadata && Object.keys(metadata).length > 0 ? { metadata } : {}),
|
||||
productTag: productTag.trim(),
|
||||
projectStatus,
|
||||
projectReview: normalizeNonIntegratedProjectReview(projectReview),
|
||||
openingReference: normalizedOpeningReference,
|
||||
@ -7182,6 +7185,11 @@ export default function NonIntegratedDetailClient({
|
||||
const [formValues, setFormValues] = useState(() =>
|
||||
createFormValuesFromProduct(existingProduct),
|
||||
);
|
||||
const [productTag, setProductTag] = useState(() =>
|
||||
Object.prototype.hasOwnProperty.call(existingProduct ?? {}, "productTag")
|
||||
? existingProduct?.productTag?.trim() ?? ""
|
||||
: "",
|
||||
);
|
||||
const formValuesRef = useRef<FormValues>(formValues);
|
||||
useEffect(() => {
|
||||
formValuesRef.current = formValues;
|
||||
@ -12187,6 +12195,7 @@ export default function NonIntegratedDetailClient({
|
||||
existingProduct?.createdBy,
|
||||
currentUserNameNormalized || currentUserIdNormalized || undefined,
|
||||
effectiveProductDevelopmentKind,
|
||||
productTag,
|
||||
nextProductMetadata,
|
||||
),
|
||||
);
|
||||
@ -12279,6 +12288,7 @@ export default function NonIntegratedDetailClient({
|
||||
productFirstQualityRows,
|
||||
productLimitRows,
|
||||
productPricingRows,
|
||||
productTag,
|
||||
productFinishedSampleShipmentRows,
|
||||
productSampleMeasurementRows,
|
||||
projectReview,
|
||||
@ -12461,6 +12471,13 @@ export default function NonIntegratedDetailClient({
|
||||
}, AUTO_SAVE_DELAY_MS);
|
||||
}
|
||||
|
||||
function updateProductTag(value: string) {
|
||||
if (isReadOnly) return;
|
||||
setProductTag(value);
|
||||
setSaveMessage("");
|
||||
setSaveMessageType("success");
|
||||
}
|
||||
|
||||
function handleTextInputCompositionStart(_: CompositionEvent<HTMLElement>) {
|
||||
isTextInputCompositionActiveRef.current = true;
|
||||
|
||||
@ -12789,6 +12806,7 @@ export default function NonIntegratedDetailClient({
|
||||
productLimitRows,
|
||||
productPricingRows,
|
||||
productSampleMeasurementRows,
|
||||
productTag,
|
||||
projectReview,
|
||||
projectStatus,
|
||||
qualityInspectionSheet,
|
||||
@ -18429,6 +18447,7 @@ export default function NonIntegratedDetailClient({
|
||||
function syncAiWhiteBackgroundSavedProduct(product: NonIntegratedProductRecord) {
|
||||
setUploadFiles(product.uploadFiles ?? []);
|
||||
setProductMetadata(product.metadata);
|
||||
setProductTag(product.productTag?.trim() ?? "");
|
||||
|
||||
if (shouldUseRemoteAssetStorage()) {
|
||||
window.dispatchEvent(new Event(NON_INTEGRATED_PRODUCTS_UPDATED));
|
||||
@ -19909,6 +19928,7 @@ export default function NonIntegratedDetailClient({
|
||||
currentUserNameNormalized || currentUserIdNormalized || undefined,
|
||||
currentUserNameNormalized || currentUserIdNormalized || undefined,
|
||||
effectiveProductDevelopmentKind,
|
||||
productTag,
|
||||
seriesComponentMetadata ?? seriesProductMetadata,
|
||||
),
|
||||
{ attachToExistingProduct: false },
|
||||
@ -26584,6 +26604,30 @@ function renderFactoryQualityWorkspace() {
|
||||
description="当前还没有上传产品主图。"
|
||||
/>
|
||||
)}
|
||||
<div className="mt-3 rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-2">
|
||||
<FieldLabel label="商品标签" />
|
||||
<div className="flex items-center gap-2">
|
||||
<InlineSelect
|
||||
ariaLabel="商品标签"
|
||||
value={productTag}
|
||||
options={NON_INTEGRATED_PRODUCT_TAG_OPTIONS}
|
||||
wrapperClassName="min-w-0 flex-1"
|
||||
readOnly={isReadOnly}
|
||||
onChange={updateProductTag}
|
||||
buttonClassName="h-8 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-2.5 text-xs text-apple-label-primary outline-none disabled:cursor-not-allowed disabled:bg-apple-bg-primary disabled:text-apple-label-secondary"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="清空商品标签"
|
||||
title="清空商品标签"
|
||||
onClick={() => updateProductTag("")}
|
||||
disabled={isReadOnly || !productTag.trim()}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-apple-lg border border-apple-separator bg-apple-bg-secondary text-apple-label-secondary transition hover:bg-apple-bg-tertiary hover:text-apple-label-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-apple-bg-secondary disabled:hover:text-apple-label-secondary"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-2 md:grid-cols-3 xl:grid-cols-5">
|
||||
|
||||
@ -23,6 +23,10 @@ import { normalizeNonIntegratedAssetUrl } from "./nonIntegratedAssetUrl";
|
||||
|
||||
export const NON_INTEGRATED_PRODUCTS_UPDATED =
|
||||
"intake:nonIntegratedProductsUpdated";
|
||||
export const NON_INTEGRATED_MODULAR_PRODUCT_TAG = "模块化";
|
||||
export const NON_INTEGRATED_PRODUCT_TAG_OPTIONS = [
|
||||
NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
] as const;
|
||||
export const NON_INTEGRATED_DUPLICATE_SPU_MESSAGE =
|
||||
"SPU 已存在,不能重复新建。删除原记录后才可以重新新建。";
|
||||
export const NON_INTEGRATED_DEFAULT_BRAND = "其他品牌-普通大货";
|
||||
@ -655,6 +659,7 @@ export type NonIntegratedProductRecord = {
|
||||
id: string;
|
||||
productDevelopmentKind?: "product" | "series";
|
||||
metadata?: Record<string, unknown>;
|
||||
productTag?: string;
|
||||
spu: string;
|
||||
projectStatus: NonIntegratedProjectStatus;
|
||||
projectReview: NonIntegratedProjectReview;
|
||||
@ -1187,6 +1192,18 @@ function normalizeMetadataRecord(value: unknown) {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function shouldDefaultModularProductTag(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
) {
|
||||
if (!metadata) return false;
|
||||
|
||||
return Boolean(
|
||||
typeof metadata.modularSpu === "string" ||
|
||||
metadata.source === "draft_group_modular_series" ||
|
||||
typeof metadata.componentDetailType === "string",
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeTupleMap<TLength extends 3 | 4>(
|
||||
value: unknown,
|
||||
tupleLength: TLength,
|
||||
@ -2229,12 +2246,20 @@ function toProductRecord(
|
||||
normalizedPreviewImage,
|
||||
normalizedPreviewImageName,
|
||||
);
|
||||
const metadata = normalizeMetadataRecord(item.metadata);
|
||||
const hasProductTag = Object.prototype.hasOwnProperty.call(item, "productTag");
|
||||
const productTag = hasProductTag
|
||||
? normalizeString(item.productTag)
|
||||
: shouldDefaultModularProductTag(metadata)
|
||||
? NON_INTEGRATED_MODULAR_PRODUCT_TAG
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
productDevelopmentKind:
|
||||
item.productDevelopmentKind === "series" ? "series" : "product",
|
||||
metadata: normalizeMetadataRecord(item.metadata),
|
||||
metadata,
|
||||
...(productTag !== undefined ? { productTag } : {}),
|
||||
spu: item.spu,
|
||||
projectStatus: normalizeNonIntegratedProjectStatus(item.projectStatus),
|
||||
projectReview: normalizeNonIntegratedProjectReview(item.projectReview),
|
||||
|
||||
@ -166,11 +166,10 @@ export default function RegisterPage() {
|
||||
label="工号"
|
||||
id="jobNumber"
|
||||
type="text"
|
||||
placeholder="请输入员工工号"
|
||||
placeholder="内部员工可填写工号,外部人员可不填"
|
||||
value={jobNumber}
|
||||
onChange={(event) => setJobNumber(event.target.value)}
|
||||
required
|
||||
helperText="系统会用工号匹配员工库,匹配成功后仍需管理员激活"
|
||||
helperText="填写工号会匹配员工库;不填写工号时,账号提交后等待管理员激活"
|
||||
variant="filled"
|
||||
size="large"
|
||||
containerClassName={authFieldClass}
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
} from '@/lib/open-product/asset-coding'
|
||||
import {
|
||||
NON_INTEGRATED_DEFAULT_BRAND,
|
||||
NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
buildNonIntegratedProductId,
|
||||
createDefaultDocumentWorkspaceConfirmations,
|
||||
createEmptyNonIntegratedOpeningReference,
|
||||
@ -1469,6 +1470,7 @@ function buildSeriesComponentDetailProductPayload(input: {
|
||||
componentSpu: spu,
|
||||
finishedProductSpu,
|
||||
},
|
||||
productTag: NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
spu,
|
||||
projectStatus: 'draft',
|
||||
projectReview: createEmptyNonIntegratedProjectReview(),
|
||||
@ -1575,6 +1577,7 @@ function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: {
|
||||
}): Record<string, unknown> {
|
||||
const payload = isRecord(input.record.payload) ? input.record.payload : {}
|
||||
const metadata = isRecord(payload.metadata) ? payload.metadata : {}
|
||||
const hasProductTag = Object.prototype.hasOwnProperty.call(payload, 'productTag')
|
||||
const openingReference = isRecord(payload.openingReference) ? payload.openingReference : {}
|
||||
const spu = resolveSeriesComponentSpu(input.series)
|
||||
const finishedProductSpu = resolveSeriesFinishedProductSpu(input.series)
|
||||
@ -1610,6 +1613,11 @@ function buildSeriesComponentDetailProductPayloadFromExistingRecord(input: {
|
||||
componentSpu: spu,
|
||||
finishedProductSpu,
|
||||
},
|
||||
productTag: hasProductTag
|
||||
? typeof payload.productTag === 'string'
|
||||
? payload.productTag
|
||||
: ''
|
||||
: NON_INTEGRATED_MODULAR_PRODUCT_TAG,
|
||||
spu: persistedSpu,
|
||||
productCode: spu,
|
||||
openingReference: {
|
||||
|
||||
@ -105,11 +105,13 @@ function createMeasurementRow(
|
||||
function createProduct(
|
||||
skuItems: NonIntegratedSkuItem[],
|
||||
pricingRows = skuItems.map((sku) => createPricingRow(sku)),
|
||||
productTag = '',
|
||||
): JushuitanProductPricingPushInput['product'] {
|
||||
return {
|
||||
id: 'product-1',
|
||||
spu: 'GBGJ-CS0002',
|
||||
brand: '顾家',
|
||||
productTag,
|
||||
productCategory: '茶几',
|
||||
productionType: '成品',
|
||||
skuItems,
|
||||
@ -202,6 +204,62 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
properties_value: '胡桃色;140/60/78',
|
||||
},
|
||||
])
|
||||
expect(
|
||||
(readBiz(fetchMock, itemSkuCallIndex).items as Array<Record<string, unknown>>)[0],
|
||||
).not.toHaveProperty('labels')
|
||||
})
|
||||
|
||||
it('商品标签有值时普通商品资料的每个 SKU 都推送 labels', async () => {
|
||||
const fetchMock = createSuccessFetchMock()
|
||||
global.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
const skuA = createSku({
|
||||
id: 'normal-a',
|
||||
skuCode: 'CS000201',
|
||||
barcode: 'CS000201N1280V01',
|
||||
})
|
||||
const skuB = createSku({
|
||||
id: 'normal-b',
|
||||
skuCode: 'CS000202',
|
||||
barcode: 'CS000202N1280V01',
|
||||
})
|
||||
|
||||
await new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product: createProduct([skuA, skuB], undefined, '模块化'),
|
||||
targetSkuIds: [skuA.id, skuB.id],
|
||||
})
|
||||
|
||||
const itemSkuCallIndex = findCallIndex(fetchMock, '/open/jushuitan/itemsku/upload')
|
||||
expect(readBiz(fetchMock, itemSkuCallIndex).items).toMatchObject([
|
||||
{
|
||||
sku_id: skuA.barcode,
|
||||
labels: ['模块化'],
|
||||
},
|
||||
{
|
||||
sku_id: skuB.barcode,
|
||||
labels: ['模块化'],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('商品标签清空时不推 labels,也不使用旧核价行标签兜底', async () => {
|
||||
const fetchMock = createSuccessFetchMock()
|
||||
global.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
const normalSku = createSku({ id: 'normal-sku' })
|
||||
await new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product: createProduct(
|
||||
[normalSku],
|
||||
[{ ...createPricingRow(normalSku), productTag: '旧标签' }],
|
||||
'',
|
||||
),
|
||||
targetSkuIds: [normalSku.id],
|
||||
})
|
||||
|
||||
const itemSkuCallIndex = findCallIndex(fetchMock, '/open/jushuitan/itemsku/upload')
|
||||
expect(
|
||||
(readBiz(fetchMock, itemSkuCallIndex).items as Array<Record<string, unknown>>)[0],
|
||||
).not.toHaveProperty('labels')
|
||||
})
|
||||
|
||||
it('成本价按系统原值推送,不再除以 0.9', async () => {
|
||||
@ -619,7 +677,7 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
})
|
||||
|
||||
const result = await new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product: createProduct([comboA, comboB, comboC]),
|
||||
product: createProduct([comboA, comboB, comboC], undefined, '模块化'),
|
||||
targetSkuIds: [comboA.id],
|
||||
})
|
||||
|
||||
@ -638,14 +696,17 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
{
|
||||
sku_id: comboA.barcode,
|
||||
properties_value: '胡桃色;140/60/78',
|
||||
labels: ['模块化'],
|
||||
},
|
||||
{
|
||||
sku_id: comboB.barcode,
|
||||
properties_value: '胡桃色;140/60/78',
|
||||
labels: ['模块化'],
|
||||
},
|
||||
{
|
||||
sku_id: comboC.barcode,
|
||||
properties_value: '胡桃色;140/60/78',
|
||||
labels: ['模块化'],
|
||||
},
|
||||
])
|
||||
expect(readBiz(fetchMock, combineSkuCallIndex)).toMatchObject(
|
||||
@ -654,7 +715,7 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
i_id: 'GBGJ-CS000201',
|
||||
name: 'GBGJ-款式一 140/60/78 胡桃色',
|
||||
properties_value: '胡桃色;140/60/78',
|
||||
labels: '双包/多包',
|
||||
labels: ['模块化'],
|
||||
items: [
|
||||
{ src_sku_id: comboA.barcode, qty: 1, sale_price: 0 },
|
||||
{ src_sku_id: comboB.barcode, qty: 1, sale_price: 0 },
|
||||
@ -702,7 +763,7 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
})
|
||||
|
||||
const result = await new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product: createProduct([combineSku]),
|
||||
product: createProduct([combineSku], undefined, '模块化'),
|
||||
targetSkuIds: [combineSku.id],
|
||||
})
|
||||
|
||||
@ -721,7 +782,7 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
sku_id: 'MKH-DNZ0384260600002',
|
||||
i_id: 'MKH-DNZ0384260600002',
|
||||
name: 'MKH-DNZ0384260600002',
|
||||
labels: '模块化组合',
|
||||
labels: ['模块化'],
|
||||
items: [
|
||||
{ src_sku_id: 'DNZ038502N1437V01', qty: 1, sale_price: 0 },
|
||||
{ src_sku_id: 'DNZ038401N1440V01', qty: 1, sale_price: 0 },
|
||||
|
||||
@ -21,6 +21,7 @@ type JushuitanProductPricingPushProduct = {
|
||||
id: string
|
||||
spu: string
|
||||
brand: string
|
||||
productTag?: string
|
||||
productCategory?: string
|
||||
productionType?: string
|
||||
openingReference?: {
|
||||
@ -84,7 +85,7 @@ type JushuitanCombineItem = {
|
||||
i_id: string
|
||||
name: string
|
||||
properties_value: string
|
||||
labels: string
|
||||
labels?: string[]
|
||||
enabled: string
|
||||
items: Array<{
|
||||
src_sku_id: string
|
||||
@ -664,6 +665,7 @@ function buildNormalItem(
|
||||
product: JushuitanProductPricingPushProduct,
|
||||
row: NormalizedPushRow,
|
||||
) {
|
||||
const productTag = resolveProductTag(product)
|
||||
const item: Record<string, unknown> = {
|
||||
sku_id: requireText(row.sku.barcode, '条码'),
|
||||
i_id: requireText(product.spu, 'SPU'),
|
||||
@ -677,6 +679,7 @@ function buildNormalItem(
|
||||
setIfText(item, 'other_10', row.sku.material)
|
||||
setIfText(item, 'other_8', row.sku.setCount || '1')
|
||||
setIfText(item, 'brand', product.brand)
|
||||
if (productTag) item.labels = [productTag]
|
||||
setIfText(item, 'properties_value', buildPropertiesValue(row.sku))
|
||||
setIfText(item, 'other_1', `${row.sku.model.trim()}${row.factoryRow.supplierCode.trim()}`)
|
||||
setIfNumber(item, 'l', row.measurementRow?.lengthCm)
|
||||
@ -1105,7 +1108,22 @@ function resolveComponentQuantity(component: { quantity?: string }) {
|
||||
return quantity && quantity > 0 ? quantity : 1
|
||||
}
|
||||
|
||||
function buildModularCombineItems(rows: NormalizedPushRow[]): JushuitanCombineItem[] {
|
||||
function resolveProductTag(product: JushuitanProductPricingPushProduct) {
|
||||
if (Object.prototype.hasOwnProperty.call(product, 'productTag')) {
|
||||
return product.productTag?.trim() ?? ''
|
||||
}
|
||||
|
||||
return firstNonEmpty(
|
||||
product.pricingRows.map((row) => row.productTag?.trim() ?? ''),
|
||||
)
|
||||
}
|
||||
|
||||
function buildModularCombineItems(
|
||||
product: JushuitanProductPricingPushProduct,
|
||||
rows: NormalizedPushRow[],
|
||||
): JushuitanCombineItem[] {
|
||||
const productTag = resolveProductTag(product)
|
||||
|
||||
return rows
|
||||
.filter((row) => hasModularCombineComponents(row.sku))
|
||||
.map((row) => {
|
||||
@ -1127,7 +1145,7 @@ function buildModularCombineItems(rows: NormalizedPushRow[]): JushuitanCombineIt
|
||||
i_id: requireText(combineSkuId, '组合款式编码'),
|
||||
name: requireText(combineSkuId, '组合商品名称'),
|
||||
properties_value: '',
|
||||
labels: '模块化组合',
|
||||
...(productTag ? { labels: [productTag] } : {}),
|
||||
enabled: '启用',
|
||||
items,
|
||||
}
|
||||
@ -1138,7 +1156,8 @@ function buildCombineItems(
|
||||
product: JushuitanProductPricingPushProduct,
|
||||
rows: NormalizedPushRow[],
|
||||
): JushuitanCombineItem[] {
|
||||
const modularItems = buildModularCombineItems(rows)
|
||||
const productTag = resolveProductTag(product)
|
||||
const modularItems = buildModularCombineItems(product, rows)
|
||||
const rowBySkuId = new Map(rows.map((row) => [row.sku.id, row]))
|
||||
const packageGroupKeys = new Set(
|
||||
rows
|
||||
@ -1181,7 +1200,7 @@ function buildCombineItems(
|
||||
i_id: requireText(combineSkuId, '组合款式编码'),
|
||||
name: requireText(combineName, '组合商品名称'),
|
||||
properties_value: buildPropertiesValue(firstSku),
|
||||
labels: '双包/多包',
|
||||
...(productTag ? { labels: [productTag] } : {}),
|
||||
enabled: '启用',
|
||||
items: groupSkus.map((sku) => ({
|
||||
src_sku_id: requireText(sku.barcode, '子商品编码'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user