232 lines
14 KiB
Plaintext
232 lines
14 KiB
Plaintext
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
|
|
<cfheader name="Content-Type" value="application/json; charset=utf-8">
|
|
|
|
<cfparam name="url.contract_id" default="">
|
|
<cfparam name="url.mode" default="partial">
|
|
<cfset body = toString(getHttpRequestData().content)>
|
|
<cfset data = deserializeJSON(body)>
|
|
<cfset ops = structKeyExists(data, "ops") ? data.ops : []>
|
|
<cfset supplementId = structKeyExists(data, "supplement_id") ? data.supplement_id : "">
|
|
|
|
<cfset result = {ok: false, error: ""}>
|
|
|
|
<cftry>
|
|
<cfif NOT len(url.contract_id)>
|
|
<cfset result.error = "contract_id required">
|
|
<cfelseif NOT len(supplementId)>
|
|
<cfset result.error = "supplement_id required (JSON body)">
|
|
<cfelse>
|
|
<!--- Идемпотентность: проверка что supplement ещё не обработан --->
|
|
<cfquery name="dupCheck" datasource="baza">
|
|
SELECT COUNT(*) as cnt FROM spec_events
|
|
WHERE supplement_id = <cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">
|
|
</cfquery>
|
|
<cfif dupCheck.cnt GT 0>
|
|
<cfset result = {ok: true, skipped: true, message: "supplement already processed", supplement_id: supplementId}>
|
|
<cfelse>
|
|
<cftransaction>
|
|
<cfset seq = 0>
|
|
<cfquery name="sq" datasource="baza">
|
|
SELECT COALESCE(MAX(seq), 0) as m FROM spec_events
|
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
</cfquery>
|
|
<cfset seq = sq.m>
|
|
|
|
<!--- full_replace: guard от пустого ops --->
|
|
<cfif url.mode EQ "full_replace" AND arrayLen(ops) EQ 0>
|
|
<cfthrow message="full_replace: LLM вернул пустой ops, спецификация НЕ изменена">
|
|
</cfif>
|
|
|
|
<!--- full_replace: явные DELETE для аудита --->
|
|
<cfif url.mode EQ "full_replace">
|
|
<cfquery name="curRows" datasource="baza">
|
|
SELECT name_hash, name, price, qty, sum, date_start
|
|
FROM spec_current
|
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
</cfquery>
|
|
<cfloop query="curRows">
|
|
<cfset seq++>
|
|
<cfquery datasource="baza">
|
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
|
'DELETE',
|
|
<cfqueryparam value="#curRows.name_hash#" cfsqltype="cf_sql_varchar">,
|
|
'full_replace',
|
|
'applied'
|
|
)
|
|
</cfquery>
|
|
</cfloop>
|
|
<cfquery datasource="baza">
|
|
DELETE FROM spec_current WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
</cfquery>
|
|
</cfif>
|
|
|
|
<!--- Применить операции --->
|
|
<cfset summary = {added: 0, updated: 0, deleted: 0, unresolved: 0}>
|
|
|
|
<!--- Pre-fetch: собрать все target_hash для UPDATE, один SELECT вместо N+1 --->
|
|
<cfset updateHashes = []>
|
|
<cfloop array="#ops#" index="op">
|
|
<cfif op.action EQ "UPDATE">
|
|
<cfset arrayAppend(updateHashes, op.target_hash)>
|
|
</cfif>
|
|
</cfloop>
|
|
<cfset oldValues = {}>
|
|
<cfif arrayLen(updateHashes) GT 0>
|
|
<cfquery name="oldBatch" datasource="baza">
|
|
SELECT name_hash, price, qty, sum, date_start FROM spec_current
|
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
AND name_hash IN (<cfloop from="1" to="#arrayLen(updateHashes)#" index="hi"><cfif hi GT 1>,</cfif><cfqueryparam value="#updateHashes[hi]#" cfsqltype="cf_sql_varchar"></cfloop>)
|
|
</cfquery>
|
|
<cfloop query="oldBatch">
|
|
<cfset oldValues[oldBatch.name_hash] = {price: oldBatch.price, qty: oldBatch.qty, sum: oldBatch.sum, date_start: oldBatch.date_start}>
|
|
</cfloop>
|
|
</cfif>
|
|
|
|
<cfloop array="#ops#" index="op">
|
|
<cfset seq++>
|
|
<cfset action = op.action>
|
|
|
|
<cfif action EQ "ADD">
|
|
<cfset nr = op.new_row>
|
|
<cfset nm = structKeyExists(nr, 'name') ? nr.name : ''>
|
|
<cfset pr = (structKeyExists(nr, 'price') AND isNumeric(nr.price)) ? nr.price : ''>
|
|
<cfset qt = (structKeyExists(nr, 'qty') AND isNumeric(nr.qty)) ? nr.qty : ''>
|
|
<cfset sm = (structKeyExists(nr, 'sum') AND isNumeric(nr.sum)) ? nr.sum : ''>
|
|
<cfset ds = (structKeyExists(nr, 'date_start') AND len(nr.date_start)) ? nr.date_start : ''>
|
|
|
|
<cfquery name="evt" datasource="baza">
|
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
|
'ADD',
|
|
<cfqueryparam value="#serializeJSON(nr)#" cfsqltype="cf_sql_varchar">::jsonb,
|
|
<cfqueryparam value="#structKeyExists(op,'comment') ? op.comment : ''#" cfsqltype="cf_sql_varchar">,
|
|
'applied'
|
|
)
|
|
RETURNING id
|
|
</cfquery>
|
|
|
|
<cfquery datasource="baza">
|
|
INSERT INTO spec_current (contract_id, name_hash, name, price, qty, sum, date_start, last_event_id)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
md5(lower(trim(<cfqueryparam value="#nm#" cfsqltype="cf_sql_varchar">)) || coalesce(<cfqueryparam value="#ds#" cfsqltype="cf_sql_varchar">, '')),
|
|
<cfqueryparam value="#nm#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#pr#" cfsqltype="cf_sql_float" null="#len(pr) EQ 0#">,
|
|
<cfqueryparam value="#qt#" cfsqltype="cf_sql_float" null="#len(qt) EQ 0#">,
|
|
<cfqueryparam value="#sm#" cfsqltype="cf_sql_float" null="#len(sm) EQ 0#">,
|
|
<cfqueryparam value="#ds#" cfsqltype="cf_sql_varchar" null="#len(ds) EQ 0#">,
|
|
<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">
|
|
)
|
|
</cfquery>
|
|
<cfset summary.added++>
|
|
|
|
<cfelseif action EQ "UPDATE">
|
|
<cfset th = op.target_hash>
|
|
<cfif NOT structKeyExists(oldValues, th)>
|
|
<!--- target_hash не найден — создаём UNRESOLVED вместо падения --->
|
|
<cfquery datasource="baza">
|
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
|
'UNRESOLVED',
|
|
<cfqueryparam value="#serializeJSON(structKeyExists(op,'new_values') ? op.new_values : {})#" cfsqltype="cf_sql_varchar">::jsonb,
|
|
<cfqueryparam value="UPDATE target_hash not found: #th#" cfsqltype="cf_sql_varchar">,
|
|
'pending'
|
|
)
|
|
</cfquery>
|
|
<cfset summary.unresolved++>
|
|
<cfelse>
|
|
<cfset old = oldValues[th]>
|
|
<cfset nv = structKeyExists(op, 'new_values') ? op.new_values : {}>
|
|
<cfset newPrice = structKeyExists(nv, 'price') AND isNumeric(nv.price) ? nv.price : old.price>
|
|
<cfset newQty = structKeyExists(nv, 'qty') AND isNumeric(nv.qty) ? nv.qty : old.qty>
|
|
<cfset newSum = structKeyExists(nv, 'sum') AND isNumeric(nv.sum) ? nv.sum : old.sum>
|
|
<cfset newDate = structKeyExists(nv, 'date_start') AND len(nv.date_start) ? nv.date_start : old.date_start>
|
|
|
|
<cfquery name="evt" datasource="baza">
|
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, new_values, comment, status)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
|
'UPDATE',
|
|
<cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#serializeJSON(nv)#" cfsqltype="cf_sql_varchar">::jsonb,
|
|
<cfqueryparam value="#structKeyExists(op,'comment') ? op.comment : ''#" cfsqltype="cf_sql_varchar">,
|
|
'applied'
|
|
)
|
|
RETURNING id
|
|
</cfquery>
|
|
|
|
<cfquery datasource="baza">
|
|
UPDATE spec_current SET
|
|
price = <cfqueryparam value="#newPrice#" cfsqltype="cf_sql_float" null="#NOT len(newPrice)#">,
|
|
qty = <cfqueryparam value="#newQty#" cfsqltype="cf_sql_float" null="#NOT len(newQty)#">,
|
|
sum = <cfqueryparam value="#newSum#" cfsqltype="cf_sql_float" null="#NOT len(newSum)#">,
|
|
date_start = <cfqueryparam value="#newDate#" cfsqltype="cf_sql_varchar" null="#NOT len(newDate)#">,
|
|
last_event_id = <cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">,
|
|
updated_at = now()
|
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
|
|
</cfquery>
|
|
<cfset summary.updated++>
|
|
</cfif>
|
|
|
|
<cfelseif action EQ "DELETE">
|
|
<cfset th = op.target_hash>
|
|
<cfquery datasource="baza">
|
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
|
'DELETE',
|
|
<cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#structKeyExists(op,'comment') ? op.comment : ''#" cfsqltype="cf_sql_varchar">,
|
|
'applied'
|
|
)
|
|
</cfquery>
|
|
<cfquery datasource="baza">
|
|
DELETE FROM spec_current
|
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
|
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
|
|
</cfquery>
|
|
<cfset summary.deleted++>
|
|
|
|
<cfelseif action EQ "UNRESOLVED">
|
|
<cfquery datasource="baza">
|
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
|
|
VALUES (
|
|
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
|
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
|
'UNRESOLVED',
|
|
<cfqueryparam value="#serializeJSON(structKeyExists(op,'new_values') ? op.new_values : {})#" cfsqltype="cf_sql_varchar">::jsonb,
|
|
<cfqueryparam value="#structKeyExists(op,'reason') ? op.reason : ''#" cfsqltype="cf_sql_varchar">,
|
|
'pending'
|
|
)
|
|
</cfquery>
|
|
<cfset summary.unresolved++>
|
|
</cfif>
|
|
</cfloop>
|
|
|
|
<cfset result = {ok: true, contract_id: url.contract_id, supplement_id: supplementId, summary: summary}>
|
|
</cftransaction>
|
|
</cfif>
|
|
</cfif>
|
|
<cfcatch>
|
|
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
|
|
</cfcatch>
|
|
</cftry>
|
|
|
|
<cfoutput>#serializeJSON(result)#</cfoutput>
|