021 dimensions dimension1

This commit is contained in:
2026-07-20 11:06:58 +03:00
parent 90aa268918
commit 3bc7cea1d1
16 changed files with 2121 additions and 185 deletions
+78 -11
View File
@@ -8,6 +8,7 @@
<!--- Set up the application. --->
<cfset this.Name = "PAYG-REPORT" />
<cfset this.applicationTimeout = createTimeSpan( 0, 3, 0, 0 ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 0, 10 ) />
<cfset this.sessionmanagement="Yes"/>
<cfset this.clientmanagement="No"/>
<cfset this.sessiontimeout=CreateTimeSpan(0, 1, 0, 0)/>
@@ -24,7 +25,8 @@
<cfset this.access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token" />
<cfset this.idpCertUrl = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs" />
<cfset this.client_secret = createObject("java", "java.lang.System").getEnv("IDP_CLIENT_SECRET")/>
<cfset this.iam_url = "https://auth-api.ngcloud.ru/api/v1/auth/user"/>
<!--- <cfset this.iam_url = "https://auth-api.ngcloud.ru/api/v1/auth/user"/> --->
<cfset this.iamServiceUrl = "https://auth-api-dev.ngcloud.ru/api/v1/auth/user "/>
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME##CGI.SCRIPT_NAME#"/><!--- куда возвращаться от IDP --->
<cfset this.datasource = "rpt"/><!--- к буферной базе --->
@@ -38,8 +40,7 @@
<!--- *** проверить, что есть - если есть, не создавать --->
<cffunction
<cffunction
name="OnApplicationStart"
access="public"
returntype="boolean"
@@ -47,11 +48,23 @@
hint="Fires when the application is first created.">
<!--- *** мы не проверяем срок жизни сертификата и его отзыв--->
<!--- Obtain idp certificate OnApplicationStart --->
<cfset application.idpCertificate = getIdpCertificate(this.idpCertUrl)/>
<!--- <cfset this.iamServiceUrl = locateIamService()/> --->
<!--- <cfreturn super.onApplicationStart() /> --->
<cfreturn true />
</cffunction>
<cffunction name="onRequestStart" returnType="boolean">
<cfargument name="targetPage" type="string" required="true" />
<cfif structKeyExists(url, "application_reset")>
<cfset ApplicationStop() />
<cflocation url="index.cfm" addtoken="false" />
</cfif>
<cfreturn true />
</cffunction>
<cffunction
name="OnRequest"
@@ -77,7 +90,7 @@
<!--- global settings --->
<cfset request.RECORDS_PER_PAGE=500/>
<cfset request.APP_VERSION="0.00.020"/>
<cfset request.APP_VERSION="0.00.021"/>
<cfheader name="X-Application-Version" value=#request.APP_VERSION#/>
<cfset request.STAND=getStand()/>
@@ -177,15 +190,29 @@
<!--- Вариант: если есть Authorization:bearer, идем с ним к IAM --->
<cfscript>
var requestData = GetHttpRequestData();
var headers = requestData.headers;
if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК
var authorizationHeader = headers.authorization;
//var authorizationHeader = server.system.environment?.AUTHORIZATION;
if (structKeyExists(headers,"Authorization")) {
request.auth_header = headers.Authorization;
} else {
//request.auth_header = createObject("java", "java.lang.System").getEnv("AUTHORIZATION");
request.auth_header = server.system.environment?.AUTHORIZATION; //так короче
if (isNull(request.auth_header)){ /*for local debug we can inject long-living token into environment*/
structDelete(request,"auth_header");
} else {
request.auth_header = "Bearer #request.auth_header#";
}
}
// if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК
if (structKeyExists(request,"auth_header")) { // аутентификация для клиента ЛК
var authorizationHeader = request.auth_header;
if (authorizationHeader.startsWith("Bearer ")) {
var bearerToken = Mid(authorizationHeader, 8);
writeDump(bearerToken);
//writeDump(bearerToken);
//var token=toString(binaryDecode(bearerToken, "base64"));
//writeDump(token);
//jwt = new lib.jwt(this.client_secret);
@@ -194,7 +221,7 @@
//}
var result="";
//try {
var iamService = new http(method = "GET", charset = "utf-8", url = #this.iam_url#, timeout="5");
var iamService = new http(method = "GET", charset = "utf-8", url = #this.iamServiceUrl#, timeout="5");
iamService.addParam(type = "HEADER", name = "Accept", value = "application/json");
iamService.addParam(type = "HEADER", name = "Authorization", value = "#authorizationHeader#"); //passthrough
//writedump(this.iamServiceUrl);abort;
@@ -206,7 +233,7 @@
//writedump(this.iamServiceUrl);
//writedump(resp);
var iamStatusCode = (isValid("integer", prefix.status_code)) ? val(prefix.status_code) : 500;
throw(message="IAM error", detail="URL: #this.iam_url# Status Code: #prefix.status_code#");
throw(message="IAM error", detail="URL: #this.iamServiceUrl# Status Code: #prefix.status_code#");
//abort;
//throw("IDP response not OK");
}
@@ -314,6 +341,10 @@
}
request.auth.wz='WZ01348'; //*******************
request.auth.wz='WZ01005'; //*******************
request.auth.wz='WZ01653'; //*******************
request.auth.wz='WZ02319'; //*******************
request.auth.wz='WZ01762'; //*******************
</cfscript>
@@ -519,4 +550,40 @@
</cftry>
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
</cffunction>
<!--- как определить стенд? Из персистенса. Из окружения --->
<!--- <cffunction name="locateIamService"><!--- *** архитектурный колхоз --->
<!--- почему-то мы не видим других функций, когда нас вызывают onApplicationStart. С другой стороны, а почему вообще должны вызывать без перезагрузки приложения --->
<cfset var stand=""/>
<!--- дублируем код, потому что getStand отсюда почему-то не виден --->
<cftry>
<cfquery name=qConfig>
select value as stand from config
where name='STAND'
</cfquery>
<cfset stand = qConfig.stand/>
<cfswitch expression=#stand#>
<cfcase value=",dev">
<cfreturn "https://auth-api-dev.ngcloud.ru/api/v1" />
</cfcase>
<cfcase value="test">
<cfreturn "https://auth-api-test.ngcloud.ru/api/v1" />
</cfcase>
<cfcase value="prod">
<cfreturn "https://auth-api.ngcloud.ru/api/v1" />
</cfcase>
<cfdefaultcase><!--- proceed to the legacy locator ---></cfdefaultcase>
</cfswitch>
<cfcatch type="ANY">
<!--- do nothing, proceed to the legacy locator --->
<cfrethrow/>
<cfthrow message="IAM service unavailable" detail="there is no IAM for stand #stand# defined"/>
</cfcatch>
</cftry>
</cffunction> --->
</cfcomponent>
+205
View File
@@ -0,0 +1,205 @@
DROP FOREIGN TABLE IF EXISTS vmreports.compute CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.storage CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.host CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.vm_gpu CASCADE;
DROP FOREIGN TABLE IF EXISTS vmreports.vm_power_state CASCADE;
--------------------------------------------------------------------------------
CREATE SCHEMA IF NOT EXISTS vmreports;
ALTER SCHEMA vmreports OWNER TO dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,appserver,reader,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver,reader;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT, REFERENCES ON TABLES TO appserver,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi, appserver;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- TOC entry 721 (class 1259 OID 9816992)
-- Name: compute; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.compute (
"Name" character varying(255),
"UUID_VM" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Host" character varying(255),
"Host_Model" character varying(255),
"CPU_Model" character varying(255),
"Host_CPU_Cores" double precision,
"Host_CPU_Sockets" double precision,
"Host_CPU_Speed" double precision,
"Host_RAM" double precision,
"Host_CPU_Used" double precision,
"Host_CPU_Used_Avg" double precision,
"VDC_CPU_Reserv" double precision,
"VDC_CPU_Speed" double precision,
"VM_vCPU_count" double precision,
"VM_CPU_Used" double precision,
"VM_CPU_Used_AVG" double precision,
"VM_CPU_Reserv_GHz" double precision,
"VM_CPU_Reserv_perc" double precision,
"VM_RAM_Allocated" double precision,
"VM_RAM_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.compute OWNER TO dbo;
--
-- TOC entry 722 (class 1259 OID 9817004)
-- Name: storage; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.storage (
"Name" character varying(255),
"VM_UUID" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Disk" character varying(255),
"Storage_Policy" character varying(255),
"Disk_Allocated" double precision,
"Disk_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.storage OWNER TO dbo;
--
-- TOC entry 720 (class 1259 OID 9816984)
-- Name: host; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.host (
"Name" character varying(255),
"Model" character varying(255),
"Cluster" character varying(255),
"Maintenance" character varying(255),
"CPU_Cores" double precision,
"CPU_Sockets" double precision,
"CPU_Speed" double precision,
"RAM_Total" double precision,
"RAM_Used" double precision,
"CPU_Used" double precision,
"CPU_Used_AVG" double precision,
"CPU_Contention_95" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
"Cluster_v2" character varying(255),
"HT_Active" boolean,
"CPUtoVM" double precision,
"RAMtoVM" double precision
);
ALTER TABLE vmreports.host OWNER TO dbo;
--
-- TOC entry 733 (class 1259 OID 11093092)
-- Name: vm_gpu; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_gpu (
"Name" character varying(255),
"ClientID" character varying(255),
"VCD" character varying(255),
"Org" character varying(255),
"OrgVDC" character varying(255),
"vCenter" character varying(255),
"GPU" jsonb,
"OrgStatus" character varying(255),
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.vm_gpu OWNER TO dbo;
--
-- TOC entry 732 (class 1259 OID 11093087)
-- Name: vm_power_state; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_power_state (
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
value double precision
);
ALTER TABLE vmreports.vm_power_state OWNER TO dbo;
--
-- TOC entry 4761 (class 2606 OID 9816998)
-- Name: compute pk_compute; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.compute
ADD CONSTRAINT pk_compute PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4759 (class 2606 OID 9816990)
-- Name: host pk_host; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.host
ADD CONSTRAINT pk_host PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4763 (class 2606 OID 9817010)
-- Name: storage pk_storage; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.storage
ADD CONSTRAINT pk_storage PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4767 (class 2606 OID 11093098)
-- Name: vm_gpu pk_vm_gpu; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_gpu
ADD CONSTRAINT pk_vm_gpu PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4765 (class 2606 OID 11093091)
-- Name: vm_power_state pk_vm_power_state; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_power_state
ADD CONSTRAINT pk_vm_power_state PRIMARY KEY ("Object_UUID", ts);
--
-- TOC entry 5168 (class 0 OID 0)
-- Dependencies: 10
-- Name: SCHEMA vmreports; Type: ACL; Schema: -; Owner: dbo
--
GRANT ALL ON SCHEMA vmreports TO nifi;
GRANT ALL ON ALL TABLES IN SCHEMA vmreports TO nifi;
GRANT SELECT, REFERENCES ON ALL TABLES IN SCHEMA vmreports TO analytics, analyst;
+128
View File
@@ -0,0 +1,128 @@
CREATE SCHEMA IF NOT EXISTS vmreports;
ALTER SCHEMA vmreports OWNER TO dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi;
GRANT ALL ON ALL TABLES IN SCHEMA vmreports TO nifi;
GRANT ALL ON ALL SEQUENCES IN SCHEMA vmreports TO nifi;
--ALTER DEFAULT PRIVILEGES IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
--ALTER DEFAULT PRIVILEGES IN SCHEMA vmreports GRANT ALL ON SEQUENCES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT ALL ON SEQUENCES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo, postgres IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO appserver;
drop table if exists vmreports.host;
create table vmreports.host(
"Name" varchar(255) NULL,
"Model" varchar(255) NULL,
"Cluster" varchar(255) NULL,
"Cluster_v2" varchar(255) NULL,
"Maintenance" varchar(255) NULL,
"CPU_Cores" float NULL, -- per host, not per socket
"CPU_Sockets" float NULL, -- not used in caclulations
"CPU_Speed" float NULL,
"RAM_Total" float NULL,
"RAM_Used" float NULL,
"CPU_Used" float NULL,
"CPU_Used_AVG" float NULL,
"CPU_Contention_95" float NULL,
"HT_Active" boolean NULL,
"CPUtoVM" float NULL,
"RAMtoVM" float NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_host PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.host to nifi;
drop table if exists vmreports.compute;
create table vmreports.compute (
"Name" varchar(255) NULL,
"UUID_VM" varchar(255) NULL,
"VDC" varchar(255) NULL,
"Organization_UUID" uuid NULL,
"Organization" varchar(255) NULL,
"WZ" varchar(255) NULL,
"Cluster" varchar(255) NULL,
"Host" varchar(255) NULL,
"Host_Model" varchar(255) NULL,
"CPU_Model" varchar(255) NULL,
"Host_CPU_Cores" float NULL,
"Host_CPU_Sockets" float NULL,
"Host_CPU_Speed" float NULL,
"Host_RAM" float NULL,
"Host_CPU_Used" float NULL,
"Host_CPU_Used_Avg" float NULL,
"VDC_CPU_Reserv" float NULL,
"VDC_CPU_Speed" float NULL,
"VM_vCPU_count" float NULL,
"VM_CPU_Used" float NULL,
"VM_CPU_Used_AVG" float NULL,
"VM_CPU_Reserv_GHz" float NULL,
"VM_CPU_Reserv_perc" float NULL,
"VM_RAM_Allocated" float NULL,
"VM_RAM_Used" float NULL,
"VM_is_ON" float NULL,
--"Summary": false,
--"Grand Total": false,
--"Group UUID": null,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_compute PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.compute to nifi;
drop table if exists vmreports.storage;
create table vmreports.storage (
"Name" varchar(255) NULL,
"VM_UUID" varchar(255) NULL,
"VDC" varchar(255) NULL,
"Organization_UUID" uuid NULL,
"Organization" varchar(255) NULL,
"WZ" varchar(255) NULL,
"Cluster" varchar(255) NULL,
"Disk" varchar(255) NULL,
"Storage_Policy" varchar(255) NULL,
"Disk_Allocated" float NULL,
"Disk_Used" float NULL,
"VM_is_ON" float NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_storage PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.storage to nifi;
drop table if exists vmreports.vm_gpu;
create table vmreports.vm_gpu (
"Name" varchar(255) NULL,
"ClientID" varchar(255) NULL, --WZ
"VCD" varchar(255) NULL, --vcloud
"Org" varchar(255) NULL,
"OrgVDC" varchar(255) NULL,
"vCenter" varchar(255) NULL,
"GPU" jsonb NULL,
"OrgStatus" varchar(255) NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_vm_gpu PRIMARY KEY (ts, "Object_UUID")
);
--grant all on vmreports.vm_gpu to nifi;
DROP TABLE IF EXISTS vmreports.vm_power_state;
CREATE TABLE IF NOT EXISTS vmreports.vm_power_state
(
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
value float,
CONSTRAINT PK_vm_power_state PRIMARY KEY ("Object_UUID", ts)
);
--grant all on vmreports.vm_power_state to nifi;
+204
View File
@@ -0,0 +1,204 @@
--
-- PostgreSQL database dump
--
--
-- TOC entry 10 (class 2615 OID 9816969)
-- Name: vmreports; Type: SCHEMA; Schema: -; Owner: dbo
--
CREATE SCHEMA IF NOT EXISTS vmreports;
ALTER SCHEMA vmreports OWNER TO dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT ALL ON TABLES TO nifi;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi, appserver;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- TOC entry 721 (class 1259 OID 9816992)
-- Name: compute; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.compute (
"Name" character varying(255),
"UUID_VM" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Host" character varying(255),
"Host_Model" character varying(255),
"CPU_Model" character varying(255),
"Host_CPU_Cores" double precision,
"Host_CPU_Sockets" double precision,
"Host_CPU_Speed" double precision,
"Host_RAM" double precision,
"Host_CPU_Used" double precision,
"Host_CPU_Used_Avg" double precision,
"VDC_CPU_Reserv" double precision,
"VDC_CPU_Speed" double precision,
"VM_vCPU_count" double precision,
"VM_CPU_Used" double precision,
"VM_CPU_Used_AVG" double precision,
"VM_CPU_Reserv_GHz" double precision,
"VM_CPU_Reserv_perc" double precision,
"VM_RAM_Allocated" double precision,
"VM_RAM_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.compute OWNER TO dbo;
--
-- TOC entry 722 (class 1259 OID 9817004)
-- Name: storage; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.storage (
"Name" character varying(255),
"VM_UUID" character varying(255),
"VDC" character varying(255),
"Organization_UUID" uuid,
"Organization" character varying(255),
"WZ" character varying(255),
"Cluster" character varying(255),
"Disk" character varying(255),
"Storage_Policy" character varying(255),
"Disk_Allocated" double precision,
"Disk_Used" double precision,
"VM_is_ON" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.storage OWNER TO dbo;
--
-- TOC entry 720 (class 1259 OID 9816984)
-- Name: host; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.host (
"Name" character varying(255),
"Model" character varying(255),
"Cluster" character varying(255),
"Maintenance" character varying(255),
"CPU_Cores" double precision,
"CPU_Sockets" double precision,
"CPU_Speed" double precision,
"RAM_Total" double precision,
"RAM_Used" double precision,
"CPU_Used" double precision,
"CPU_Used_AVG" double precision,
"CPU_Contention_95" double precision,
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
"Cluster_v2" character varying(255),
"HT_Active" boolean,
"CPUtoVM" double precision,
"RAMtoVM" double precision
);
ALTER TABLE vmreports.host OWNER TO dbo;
--
-- TOC entry 733 (class 1259 OID 11093092)
-- Name: vm_gpu; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_gpu (
"Name" character varying(255),
"ClientID" character varying(255),
"VCD" character varying(255),
"Org" character varying(255),
"OrgVDC" character varying(255),
"vCenter" character varying(255),
"GPU" jsonb,
"OrgStatus" character varying(255),
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL
);
ALTER TABLE vmreports.vm_gpu OWNER TO dbo;
--
-- TOC entry 732 (class 1259 OID 11093087)
-- Name: vm_power_state; Type: TABLE; Schema: vmreports; Owner: smishchuk
--
CREATE TABLE vmreports.vm_power_state (
"Object_UUID" uuid NOT NULL,
ts timestamp with time zone NOT NULL,
value double precision
);
ALTER TABLE vmreports.vm_power_state OWNER TO dbo;
--
-- TOC entry 4761 (class 2606 OID 9816998)
-- Name: compute pk_compute; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.compute
ADD CONSTRAINT pk_compute PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4759 (class 2606 OID 9816990)
-- Name: host pk_host; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.host
ADD CONSTRAINT pk_host PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4763 (class 2606 OID 9817010)
-- Name: storage pk_storage; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.storage
ADD CONSTRAINT pk_storage PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4767 (class 2606 OID 11093098)
-- Name: vm_gpu pk_vm_gpu; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_gpu
ADD CONSTRAINT pk_vm_gpu PRIMARY KEY (ts, "Object_UUID");
--
-- TOC entry 4765 (class 2606 OID 11093091)
-- Name: vm_power_state pk_vm_power_state; Type: CONSTRAINT; Schema: vmreports; Owner: smishchuk
--
ALTER TABLE ONLY vmreports.vm_power_state
ADD CONSTRAINT pk_vm_power_state PRIMARY KEY ("Object_UUID", ts);
--
-- TOC entry 5168 (class 0 OID 0)
-- Dependencies: 10
-- Name: SCHEMA vmreports; Type: ACL; Schema: -; Owner: dbo
--
GRANT ALL ON SCHEMA vmreports TO nifi;
+101
View File
@@ -0,0 +1,101 @@
ALTER TABLE IF EXISTS vmreports.storage
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.storage FROM analyst;
REVOKE ALL ON TABLE vmreports.storage FROM analytics;
REVOKE ALL ON TABLE vmreports.storage FROM appserver;
GRANT SELECT ON TABLE vmreports.storage TO analyst;
GRANT SELECT ON TABLE vmreports.storage TO analytics;
GRANT SELECT ON TABLE vmreports.storage TO appserver;
GRANT ALL ON TABLE vmreports.storage TO dbo;
GRANT ALL ON TABLE vmreports.storage TO designer;
GRANT ALL ON TABLE vmreports.storage TO nifi;
---------------
ALTER TABLE IF EXISTS vmreports.compute
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.compute FROM analyst;
REVOKE ALL ON TABLE vmreports.compute FROM analytics;
REVOKE ALL ON TABLE vmreports.compute FROM appserver;
GRANT SELECT ON TABLE vmreports.compute TO analyst;
GRANT SELECT ON TABLE vmreports.compute TO analytics;
GRANT SELECT ON TABLE vmreports.compute TO appserver;
GRANT ALL ON TABLE vmreports.compute TO dbo;
GRANT ALL ON TABLE vmreports.compute TO designer;
GRANT ALL ON TABLE vmreports.compute TO nifi;
--------------------
ALTER TABLE IF EXISTS vmreports.host
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.host FROM analyst;
REVOKE ALL ON TABLE vmreports.host FROM analytics;
REVOKE ALL ON TABLE vmreports.host FROM appserver;
GRANT SELECT ON TABLE vmreports.host TO analyst;
GRANT SELECT ON TABLE vmreports.host TO analytics;
GRANT SELECT ON TABLE vmreports.host TO appserver;
GRANT ALL ON TABLE vmreports.host TO dbo;
GRANT ALL ON TABLE vmreports.host TO designer;
GRANT ALL ON TABLE vmreports.host TO nifi;
------------------------------------
ALTER TABLE IF EXISTS vmreports.vm_gpu
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.vm_gpu FROM analyst;
REVOKE ALL ON TABLE vmreports.vm_gpu FROM analytics;
REVOKE ALL ON TABLE vmreports.vm_gpu FROM appserver;
GRANT SELECT ON TABLE vmreports.vm_gpu TO analyst;
GRANT SELECT ON TABLE vmreports.vm_gpu TO analytics;
GRANT SELECT ON TABLE vmreports.vm_gpu TO appserver;
GRANT ALL ON TABLE vmreports.vm_gpu TO dbo;
GRANT ALL ON TABLE vmreports.vm_gpu TO designer;
GRANT ALL ON TABLE vmreports.vm_gpu TO nifi;
-----------------
ALTER TABLE IF EXISTS vmreports.vm_power_state
OWNER to dbo;
REVOKE ALL ON TABLE vmreports.vm_power_state FROM analyst;
REVOKE ALL ON TABLE vmreports.vm_power_state FROM analytics;
REVOKE ALL ON TABLE vmreports.vm_power_state FROM appserver;
GRANT SELECT ON TABLE vmreports.vm_power_state TO analyst;
GRANT SELECT ON TABLE vmreports.vm_power_state TO analytics;
GRANT SELECT ON TABLE vmreports.vm_power_state TO appserver;
GRANT ALL ON TABLE vmreports.vm_power_state TO dbo;
GRANT ALL ON TABLE vmreports.vm_power_state TO designer;
GRANT ALL ON TABLE vmreports.vm_power_state TO nifi;
+65
View File
@@ -0,0 +1,65 @@
SET ROLE dbo;
DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
GRANT USAGE ON SCHEMA vmreports TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO appserver;
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
*/
--DROP FOREIGN TABLE IF EXISTS host;
--CREATE FOREIGN TABLE vmreports.host () SERVER billing OPTIONS (schema_name 'vmreports', table_name 'host'); -- вместо этого лучше импортировать схему
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing', updatable 'false');
-- для импорта схемы мы создаем маппинг владельцу
DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER billing;
CREATE USER MAPPING FOR PUBLIC SERVER billing OPTIONS (user 'appserver', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
--GRANT SELECT ON vmreports.host to appserver;
--DROP USER MAPPING IF EXISTS FOR dbo SERVER billing; -- мы импортировали схему, маппинг больше не нужен.
-- Но нам все равно пришлось указать пароль дважды
--DROP USER MAPPING IF EXISTS FOR appserver SERVER billing;
--CREATE USER MAPPING FOR appserver SERVER billing OPTIONS (user 'nifi', password '***');
--проверка
--set role appserver; select * from vmreports.host;
-- это если интересно, не разрешен ли локальный коннект
--SELECT * FROM pg_hba_file_rules;
/*
Довольно тонкий нюанс, легко ускользающий
ALTER DEFAULT PRIVILEGES FOR ROLE target_creator_role
IN SCHEMA schema_name
GRANT privilege_type ON TABLES TO recipient_role;
Recommended Strategy for "Any User"Because there is no "FOR ALL ROLES" option, the best practice is to have all users create objects as a shared group role.Create a group role (e.g., schema_owner_role).Grant that role to all relevant users.Instruct users to run SET ROLE schema_owner_role; before creating objects.Run ALTER DEFAULT PRIVILEGES FOR ROLE schema_owner_role ... once to cover all objects created under that role.
*/
/*
Best Practices for Role Owners:Dedicated Schema Owner: Create a dedicated, non-login role for managing database schema objects (DDL) and make it the owner of the database/schemas.Separation of Duties: Separate the application user (DML - SELECT, INSERT, UPDATE) from the owner role (DDL - CREATE, ALTER, DROP).Use ALTER DEFAULT PRIVILEGES: Set up default privileges for new objects to ensure that user roles automatically gain access to new tables or functions created by the owner role.Leverage Role Hierarchy: Create group roles (e.g., app_reader, app_writer) and grant these to individual user roles for easier permission management.Avoid Public Schema: Create specific schemas and revoke CREATE privileges on the public schema to prevent accidental object creation.Secure SUPERUSER Usage: Only use the default postgres superuser role for initial setup and critical administrative tasks. Never use it for applications.Example Implementation:sql-- Create a schema owner
CREATE ROLE ddl_user NOLOGIN;
-- Create an app user
CREATE ROLE dml_user LOGIN PASSWORD 'secure_password';
-- Assign ownership
ALTER DATABASE mydb OWNER TO ddl_user;
ALTER SCHEMA public OWNER TO ddl_user;
-- Grant future access
ALTER DEFAULT PRIVILEGES FOR ROLE ddl_user IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dml_user;
*/
+37
View File
@@ -0,0 +1,37 @@
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO dbo;
SET ROLE dbo;
DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst; -- это работает и для внешних таблиц
--ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
*/
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing', updatable 'false');
DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER billing;
CREATE USER MAPPING FOR PUBLIC SERVER billing OPTIONS (user 'nifi', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
--select * from vmreports.host;
/*
план миграции
Забекапить схему vmreports
Восстановить схему vmreports в базу биллинг
Поменять целевую базу импорта НиФи (для прода - втащить новый обмен из репозитория)
Выполнить скрипт в базе DWH - он удалит физические таблицы и создаст внешние таблицы на базу billing
*/
+113
View File
@@ -0,0 +1,113 @@
-- выполнить в базе dwh для создания внешних таблиц схемы vmreports
-- перед выполнением забекапить схему vmreports для восстановления в БД billing
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO dbo;
SET ROLE dbo;
--DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst; -- это работает и для внешних таблиц
--ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
А при смене его пароля все сломается, и поиск ошибки будет неочевиден
*/
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'vmreports'
LOOP
EXECUTE format('ALTER TABLE vmreports.%I RENAME TO %I',
rec.tablename,
'del_' || rec.tablename);
END LOOP;
END $$;
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing', updatable 'false');
DROP USER MAPPING IF EXISTS FOR PUBLIC SERVER billing;
CREATE USER MAPPING FOR PUBLIC SERVER billing OPTIONS (user 'nifi', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
--select * from vmreports.host;
/*
план миграции - черновик
Забекапить схему vmreports
Восстановить схему vmreports из бэкапа в базу биллинг
Поменять целевую базу импорта НиФи (для прода - втащить новый обмен из репозитория)
Выполнить скрипт в базе DWH - он удалит (переименует) физические таблицы и создаст внешние таблицы на базу billing
*/
/*
Исходно данные host, compute, storage, (gpu)
загружались в dwh.vmreports,
для анализа capacity
В текущей конфигурации,
чтобы избежать дублирования
таблиц, данных и обменов NiFi
Направляем загрузку данных из VROPS в схему billing.vmreports
и в базе dwh (схема vmreports) создаем Foreign Tables на billing.vmreports.
-----------------------------------------------
Миграция на проде:
убедиться, что новый обмен VROPS->billing.vmreports (billing/vmReports) загружен в NiFi Registry
найти пароль nifi (под ним коннект к localhost)
остановить загрузку из VROPS на nifi-01 (billing/vmReports),
сделать бэкап схемы dwh.vmreports (займет минут 10 ... заняло 215 сек),
Восстановить схему vmreports в БД billing - запустить восстановление (Clean Before Restore) (РИСК: восстановление может занять много времени и завершиться с ошибками, обычно так бывает)
select count (*) from vmreports.storage
Выполнить этот скрипт (заменив пароль nifi на актуальный перед выполнением)
переименовать таблицы из схемы vmreports для удаления (есть в скрипте выше)
создать Foreign Tables (коннект проверится при импорте схемы)
Проверить, что права назначились коррректно
Обновить обмен billing/vmReports на продуктиве, запустить
-----------------------------------------------------------------
Данные из billing.vmreports также реплицируются
(процессорами группы LK)
в буферную базу rpt
*/
/*
17:04:49 MSKERROR0dff1f93-153d-3a90-ed64-55d23960b88c
EvaluateJsonPath[id=0dff1f93-153d-3a90-ed64-55d23960b88c] FlowFile FlowFile[filename=06c68bec-d824-4114-913b-bda34cb4fb1d] did not have valid JSON content.
-- со второй попытки отработало
17:05:15 MSKERROR239db407-76f2-3660-7fad-c4bedff6a4f8
InvokeHTTP[id=239db407-76f2-3660-7fad-c4bedff6a4f8] Request Processing failed: FlowFile[filename=3474c340-f360-4a1e-a27e-283be41fc264]: java.net.SocketTimeoutException: timeout
- Caused by: java.net.SocketException: Socket closed
https://payg.adl.nubes.ru/payg.cfm с первого раза таймаут, со второго посчиталось
*/
/*
теперь ищу в нифи примеры с кроном
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Failed to put Records to database for FlowFile[filename=11f8176f-2a98-4199-932e-22e2fc2073b1]. Routing to failure.: org.apache.nifi.serialization.MalformedRecordException: Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Failed to put Records to database for FlowFile[filename=5039a3b6-2244-4451-81fc-deb8074f3cc7]. Routing to failure.: org.apache.nifi.serialization.MalformedRecordException: Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Record does not have a value for the UpdateKey column 'obj_uuid'
17:25:05 MSKERROR16efa0f6-f1e7-3755-04fc-5be0383244ec
PutDatabaseRecord[id=16efa0f6-f1e7-3755-04fc-5be0383244ec] Failed to put Records to database for FlowFile[filename=8c6e9efb-198d-4303-897a-53fcfdf4d0f6]. Routing to failure.: org.apache.nifi.serialization.MalformedRecordException: Record does not have a value for the UpdateKey column 'obj_uuid'
*/
+47
View File
@@ -0,0 +1,47 @@
SET ROLE dbo;
DROP SCHEMA IF EXISTS vmreports CASCADE;
CREATE SCHEMA IF NOT EXISTS vmreports AUTHORIZATION dbo;
--GRANT ALL ON SCHEMA vmreports TO postgres;
BEGIN
GRANT USAGE ON SCHEMA vmreports TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO appserver;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO appserver;
EXCEPTION END;
BEGIN
GRANT USAGE ON SCHEMA vmreports TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT SELECT ON TABLES TO nifi,analytics,analyst;
ALTER DEFAULT PRIVILEGES FOR ROLE dbo IN SCHEMA vmreports GRANT USAGE ON SEQUENCES TO nifi,analytics,analyst;
EXCEPTION WHEN others THEN END;
/*
конструкция осложняется тем, что
нужен один пользователь-владелец и второй читатель
Это вызывает множество телодвижений при создании.
Необходимость указывать пароль при локальном коннекте не очень радует
(ведь пользователь локальный, прописан на этом же сервере)
*/
--DROP FOREIGN TABLE IF EXISTS host;
--CREATE FOREIGN TABLE vmreports.host () SERVER billing OPTIONS (schema_name 'vmreports', table_name 'host'); -- вместо этого лучше импортировать схему
DROP SERVER IF EXISTS billing CASCADE;
CREATE SERVER billing
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '5432', dbname 'billing');
-- для импорта схемы мы создаем маппинг владельцу
DROP USER MAPPING IF EXISTS FOR dbo SERVER billing;
CREATE USER MAPPING FOR dbo SERVER billing OPTIONS (user 'appserver', password '***');
IMPORT FOREIGN SCHEMA vmreports FROM SERVER billing INTO vmreports;
GRANT SELECT ON vmreports.host to appserver;
DROP USER MAPPING IF EXISTS FOR dbo SERVER billing; -- мы импортировали схему, маппинг больше не нужен.
-- Но нам все равно пришлось указать пароль дважды
DROP USER MAPPING IF EXISTS FOR appserver SERVER billing;
CREATE USER MAPPING FOR appserver SERVER billing OPTIONS (user 'appserver', password '***');
--проверка
--set role appserver; select * from vmreports.host;
-- это если интересно, не разрешен ли локальный коннект
--SELECT * FROM pg_hba_file_rules;
+52
View File
@@ -0,0 +1,52 @@
SET ROLE dbo;
DROP TABLE IF EXISTS vmreports.vsan_storage CASCADE;
CREATE TABLE vmreports.vsan_storage (
"Name" varchar(255) NULL,
"ClusterNew" varchar(255) NULL,
"Total" float NULL,
"Usable" float NULL,
"Used" float NULL,
"EffectiveFree" float NULL,
"Object_UUID" uuid NOT NULL,
ts timestamptz NOT NULL
,CONSTRAINT PK_vsan_storage PRIMARY KEY (ts, "Object_UUID")
);
--ALTER TABLE vmreports.vsan_storage OWNER TO dbo;
GRANT ALL ON vmreports.vsan_storage TO nifi;
--select * from vmreports.vsan_storage;
/*
{
"type": "record",
"name": "vmreports.vsan_storage",
"namespace": "org.apache.nifi",
"fields": [
{"name":"Object_UUID", "type":{"type": "string", "logicalType": "uuid"}},
{"name":"ts","type":{"type": "long", "logicalType": "local-timestamp-millis"}},
{"name":"Name", "type":["string" ,"null"]},
{"name":"ClusterNew", "type":["string" ,"null"]},
{"name":"Total", "type":["float" ,"null"]},
{"name":"Usable", "type":["float" ,"null"]},
{"name":"Used", "type":["float" ,"null"]},
{"name":"EffectiveFree", "type":["float" ,"null"]}
]
}
*/
/*
{
"*": {
"objId":"[&1].Name",
"1":"[&1].ClusterNew",
"2":"[&1].Total",
"3":"[&1].Usable",
"4":"[&1].Used",
"5":"[&1].EffectiveFree",
"objUUID":"[&1].Object_UUID"
}
}
*/
+633
View File
@@ -0,0 +1,633 @@
{
"companyId": "1",
"contracts": [
{
"contractId": "01983721-6049-72dd-a682-a5af1bd72420",
"contractInsideId": "02319_1",
"contractOutsideId": "02319_1",
"contractStartDate": "20250801",
"isContractTerminated": false,
"CalculationPeriod": "20260701",
"additionalAgreements": [
{
"additionalAgreementId": "019b6ec1-1984-78fc-9e48-0984aa1f3ca7",
"additionalAgreementIndex": 3,
"additionalAgreementName": "Доп соглашение 3, к сделке №3768 Сделка на изменения договора",
"additionalAgreementStartDate": "",
"additionalAgreementBaseBy": "019a5ef8-eebe-76b3-acb9-b5c37748f4ef",
"isActual": true,
"CalculationPeriod": "20260701",
"services": [
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.ceph.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.8300000000000001,
"discount": 0,
"atomicCode": "paas.s3.ceph.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.ceph.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5249999999999999,
"discount": 0,
"atomicCode": "paas.s3.ceph.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.ceph.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.3538,
"discount": 0,
"atomicCode": "paas.s3.ceph.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.ceph.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 4.5750000000000002,
"discount": 0,
"atomicCode": "paas.s3.ceph.put"
}
],
"staticId": "e57bb0bd-8946-4603-95e8-c9c331378bce",
"serviceId": "2e1681d7-542e-489c-9169-de562bdca83a",
"serviceHash": "e57bb0bd-8946-4603-95e8-c9c331378bce3",
"LineCRMID": "019b6ec6-a779-7902-abeb-d2f7e746d64e",
"serviceCode": "paas.s3.ceph",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0.24125452,
"quantity": 1,
"dateStartProvision": "20260101",
"dateEndProvision": "",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2026-01-01T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 22,
"vatRate": 0.22,
"vatPerc": 22,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "2e1681d7-542e-489c-9169-de562bdca83a",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.026833333333,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5249999999999999,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.97599999999999998,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.834,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "2031f3ac-e921-4816-b91f-9b5b1cf5bf94",
"serviceId": "5271dc39-2ec5-4f20-b91e-fdffb4f363b9",
"serviceHash": "2031f3ac-e921-4816-b91f-9b5b1cf5bf943",
"LineCRMID": "019b6ec6-a82c-7902-93a3-48ba4e25d71d",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20260101",
"dateEndProvision": "",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2026-01-01T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 22,
"vatRate": 0.22,
"vatPerc": 22,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "5271dc39-2ec5-4f20-b91e-fdffb4f363b9",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 175,
"componentCode": "paas.s3.cphc.volq",
"component": "Дисковое пространство: Квота",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 2.7999999999999998,
"discount": 0,
"atomicCode": "paas.s3.cphc.volq"
}
],
"staticId": "f216f0ac-6c61-42aa-a454-56a93c707d11",
"serviceId": "776a3490-cb3a-4721-b521-7bf97073e104",
"serviceHash": "f216f0ac-6c61-42aa-a454-56a93c707d112",
"LineCRMID": "019b6ec6-a86d-7902-9ba2-defe46f2a036",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 63.928570999999998,
"quantity": 1000,
"dateStartProvision": "20250826",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-26T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное (Квота), в составе:\n Дисковое пространство: Квота - 1 ГБайт\n",
"serviceType": 2,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "776a3490-cb3a-4721-b521-7bf97073e104",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 175,
"componentCode": "paas.s3.cphc.volq",
"component": "Дисковое пространство: Квота",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 2.7999999999999998,
"discount": 0,
"atomicCode": "paas.s3.cphc.volq"
}
],
"staticId": "78707549-a675-46e0-bc5b-790ef4d21b6d",
"serviceId": "2e9369bc-7f3e-4c31-bb72-3d19e979964e",
"serviceHash": "78707549-a675-46e0-bc5b-790ef4d21b6d2",
"LineCRMID": "019b6ec6-a8ab-7902-af73-1034bf013a16",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 63.928570999999998,
"quantity": 40000,
"dateStartProvision": "20250826",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-26T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное (Квота), в составе:\n Дисковое пространство: Квота - 1 ГБайт\n",
"serviceType": 2,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "2e9369bc-7f3e-4c31-bb72-3d19e979964e",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.01,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.95999999999999996,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.640000000000001,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "20240911T125603868",
"serviceId": "a564eda1-b716-42ca-9ae7-efc49b112a58",
"serviceHash": "20240911T1256038683",
"LineCRMID": "019b6ec6-a949-7902-b473-8741f2a18efa",
"serviceCode": "paas.s3.cphc",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20250826",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-26T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "a564eda1-b716-42ca-9ae7-efc49b112a58",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.ceph.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.8,
"discount": 0,
"atomicCode": "paas.s3.ceph.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.ceph.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.ceph.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.34999999999999998,
"discount": 0,
"atomicCode": "paas.s3.ceph.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.ceph.put",
"component": "Запросы PUT, POST, PATCH",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 4.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.put"
}
],
"staticId": "20241015T124353835",
"serviceId": "5371b7cb-6f6c-4c7d-85b4-609eb52f994a",
"serviceHash": "20241015T1243538353",
"LineCRMID": "019b6ec6-aa03-7902-9c40-06f6d5cf6e13",
"serviceCode": "paas.s3.ceph",
"endCustomerName": "",
"endCustomerINN": "",
"catalogName": "Объектное хранилище",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20250801",
"dateEndProvision": "20250930",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-09-30T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "5371b7cb-6f6c-4c7d-85b4-609eb52f994a",
"dealIndex": 3768,
"CalculationPeriod": "20260701"
},
{
"components": [
{
"paramId": 69,
"componentCode": "paas.s3.ceph.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.trf"
},
{
"paramId": 70,
"componentCode": "paas.s3.ceph.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 4.5,
"discount": 0,
"atomicCode": "paas.s3.ceph.put"
},
{
"paramId": 118,
"componentCode": "paas.s3.ceph.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.8,
"discount": 0,
"atomicCode": "paas.s3.ceph.vol"
},
{
"paramId": 68,
"componentCode": "paas.s3.ceph.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.34999999999999998,
"discount": 0,
"atomicCode": "paas.s3.ceph.get"
}
],
"staticId": "e57bb0bd-8946-4603-95e8-c9c331378bce",
"serviceId": "09fd617a-9ffc-4b98-ad66-3fedac52c7b9",
"serviceHash": "e57bb0bd-8946-4603-95e8-c9c331378bce3",
"LineCRMID": "019a732b-22a2-7545-91ce-819207c779a0",
"catalogName": "Объектное хранилище",
"endCustomerName": "",
"endCustomerINN": "",
"serviceCode": "paas.s3.ceph",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20251001",
"CalculationPeriod": "20260701",
"dateEndProvision": "20251231",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-10-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-12-31T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH, в составе:\n Исходящий трафик - 1 ГБайт\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\nДисковое пространство: Объем хранения - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "09fd617a-9ffc-4b98-ad66-3fedac52c7b9",
"dealIndex": 3274
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.01,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.95999999999999996,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH, LIST",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.640000000000001,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "2031f3ac-e921-4816-b91f-9b5b1cf5bf94",
"serviceId": "539d210f-2e5a-45e2-b148-b0b0834c138c",
"serviceHash": "2031f3ac-e921-4816-b91f-9b5b1cf5bf943",
"LineCRMID": "019a732b-22d4-7545-83af-18b7f8867eaa",
"catalogName": "Объектное хранилище",
"endCustomerName": "",
"endCustomerINN": "",
"serviceCode": "paas.s3.cphc",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20251001",
"CalculationPeriod": "20260701",
"dateEndProvision": "20251231",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-10-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-12-31T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 1 ГБайт\nИсходящий трафик - 1 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nЗапросы PUT, POST, PATCH, LIST - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "539d210f-2e5a-45e2-b148-b0b0834c138c",
"dealIndex": 3274
},
{
"components": [
{
"paramId": 118,
"componentCode": "paas.s3.cphc.vol",
"component": "Дисковое пространство: Объем хранения",
"measureShort": "ГБайт",
"quantity": 40000,
"priceWVat": 1.01,
"discount": 0,
"atomicCode": "paas.s3.cphc.vol"
},
{
"paramId": 68,
"componentCode": "paas.s3.cphc.get",
"component": "Запросы GET, HEAD, OPTIONS",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 0.95999999999999996,
"discount": 0,
"atomicCode": "paas.s3.cphc.get"
},
{
"paramId": 69,
"componentCode": "paas.s3.cphc.trf",
"component": "Исходящий трафик",
"measureShort": "ГБайт",
"quantity": 1,
"priceWVat": 1.5,
"discount": 0,
"atomicCode": "paas.s3.cphc.trf"
},
{
"paramId": 70,
"componentCode": "paas.s3.cphc.put",
"component": "Запросы PUT, POST, PATCH",
"measureShort": "10тыс.шт.",
"quantity": 1,
"priceWVat": 11.640000000000001,
"discount": 0,
"atomicCode": "paas.s3.cphc.put"
}
],
"staticId": "20240911T125603868",
"serviceId": "67dffa20-a69e-449a-b02c-edf78d9ff192",
"serviceHash": "20240911T1256038683",
"LineCRMID": "01983726-3501-7496-b7f3-6f58f4d7f209",
"catalogName": "Объектное хранилище",
"endCustomerName": "",
"endCustomerINN": "",
"serviceCode": "paas.s3.cphc",
"discount": 0,
"quantity": 1,
"dateStartProvision": "20250801",
"CalculationPeriod": "20260701",
"dateEndProvision": "20250825",
"serviceStart": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-01T00:00:00.000Z"
},
"serviceEnd": {
"currentTimeZoneName": "UTC",
"ts": "2025-08-25T00:00:00.000Z"
},
"serviceCustomName": "Объектное хранилище: S3 CEPH Холодное, в составе:\n Дисковое пространство: Объем хранения - 40000 ГБайт\nЗапросы GET, HEAD, OPTIONS - 1 10тыс.шт.\nИсходящий трафик - 1 ГБайт\nЗапросы PUT, POST, PATCH - 1 10тыс.шт.\n",
"serviceType": 3,
"service_is_easy": false,
"vat": 20,
"vatRate": 0.20000000000000001,
"vatPerc": 20,
"vatFree": 0,
"priceWVat": "",
"serviceVersionGuid": "67dffa20-a69e-449a-b02c-edf78d9ff192",
"dealIndex": 1450
}
]
}
]
}
]
}
+115 -28
View File
@@ -4,7 +4,7 @@
<cfset hours=dateDiff('h',dt_start,dt_finish)/>
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
<cffunction name="snake2camel"
<!--- <cffunction name="snake2camel"
returntype="string"
output="false"
hint="convert snake style name to camel style name">
@@ -30,7 +30,7 @@
</cffunction>
<cffunction name="query2json"
<cffunction name="query2array"
output="false"
hint="print query in json format as array of structures">
<cfargument name="qry" type="query" required="true"/><!--- column names should not contain commas --->
@@ -41,11 +41,11 @@
<cfif arguments.convertSnakeToCamel>
<cfloop list=#columnsIn# item="col">
<cfset columnsOut=listAppend(columnsOut,snake2camel(col))/>
<cfset columnsOut=listAppend(columnsOut,snake2camel(lcase(col)))/>
</cfloop>
<cfset var outArray=arrayNew(1)/>
//copy fields to structure, renaming fields, this preserves data type
<!--- //copy fields to structure, renaming fields, this preserves data type --->
<cfloop query=#arguments.qry#>
<cfset var structRow=structNew()/>
<cfloop index="i" from="1" to=#listLen(columnsOut)#>
@@ -53,42 +53,129 @@
</cfloop>
<cfset arrayAppend(outArray,structRow)/>
</cfloop>
<cfreturn serializeJSON(outArray)/>
<!--- <cfdump var=#outArray#/> --->
<cfreturn outArray/>
<cfelse>
<cfreturn serializeJSON(arguments.qry,"struct")/>
<cfreturn #arguments.qry.reduce((result, row) => {
result.append(row);
return result;
}, [])#/>
</cfif>
</cffunction>
</cffunction> --->
<cfscript>
function gaussianRound(numeric number, numeric precision = 0) {
var factor = 10 ^ precision;
var scaledNumber = number * factor;
var absScaled = abs(scaledNumber);
var sign = sgn(scaledNumber);
var floored = int(absScaled);
// If it's not ending in exactly .5, use standard rounding or precision scaling
if (absScaled - floored != 0.5) {
return round(number * factor) / factor;
}
// If it is exactly .5, round towards the nearest even number
if (floored % 2 != 0) {
// Nearest even is UP
return (floored + 1) * sign / factor;
} else {
// Nearest even is DOWN
return floored * sign / factor;
}
}
</cfscript>
<cfmodule template="mod/payg.cfm"
<cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
output="report"
debug=#isDefined("DEBUG")#
/>
<cfset out={
"appVersion"=#request.APP_VERSION#,
"hours"=#hours#,
"dtStart="=#iso8601dtFormat(dt_start)#,
"dtFinish"=#iso8601dtFormat(dt_finish)#,
"vcdComputingLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"vcdStorageLoadedAt"="#iso8601dtFormat(report.qStorageAge.dt_load)#",
"s3StorageLoadedAt"="#iso8601dtFormat(report.qComputingAge.dt_load)#",
"s3OpsTrfLoadedAt"="#iso8601dtFormat(report.qS3OpsTrfAge.dt_load)#",
"records"=#report.qCharge.recordCount#,
"queryDurationMs"=#(getTickCount() - msStartAt)#,
"wz"=#request.auth.wz#,
"login"=#request.auth.login#,
"report"=#report.qCharge.reduce((result, row) => {
result.append(row);
return result;
}, [])#
}/>
<cfset temp.out=["dtStart"="#dateFormat(dt_start, "yyyy-mm-dd")#"
,"dtFinish"="#dateFormat(dt_finish, "yyyy-mm-dd")#"
,"hours"=#hours#
,"tenantWzIndex"="#tenant_wz_index#"
,"periods"=[]
]/><!--- using [] instead of {} for struct literals to obtain linked (ordered) structures: keys order preserved --->
<!--- beware nulls --->
<cfoutput query="report.qCharge" group="year">
<cfoutput group="month">
<cfset temp.period=[
"year"=#year#
,"month"=#month#
,"services"=[]
]/>
<cfset arrayAppend(temp.out.periods,temp.period)/>
<cfoutput group="svc">
<cfset var accum_cost_svc = 0/>
<!--- <cfset var accum_metric_component = 0/> --->
<cfset "temp.service"=["svc"="#svc#"
,"serviceСode"="#service_code#"
,"serviceCustomName"="#serviceCustomName#"
<!--- ,"note"="#(serviceHash EQ '')?'не найдено в договоре':''#" --->
,"hash"="#serviceHash#"
<!--- ,"metric"=#accum_metric_component# --->
,"components"=[]
]/>
<cfoutput group="code">
<cfoutput group="serviceStart">
<!--- <cfset accum_metric_component += isNumeric(metric) ? metric : 0/> --->
<cfset accum_metric_dimension_1 = 0/>
<cfset "temp.component"=["code"="#code#"
,"codeMetered"="#m_code#"
,"component"="#component#"
,"unit"="#unit#"
,"freeTier"="#free_tier#"
,"price"="#gaussianRound(discounted_price,2)#"
,"details"=[]
]/>
<cfoutput group="dimension_1">
<cfset accum_metric_dimension_1 += isNumeric(metric) ? metric : 0/>
<cfset temp.dimension=["dimension1name"="#dimension_1_name#"
,"dimension1value"="#dimension_1#"
,"dtBilledFrom"="#dateFormat(dt_billed_from,'YYYY-MM-DD')#"
,"dtBilledTo"="#dateFormat(dt_billed_to,'YYYY-MM-DD')#"
,"metric"=#(metric NEQ 0 AND isNumeric(metric))?round(metric,3):javaCast("null",0)#
,"unit"="#unit#"
<!--- ,"price"="#gaussianRound(discounted_price,2)#" --->
]/>
<cfset arrayAppend(temp.component.details, temp.dimension)/>
</cfoutput><!--- dimension_1 --->
<!--- считаем, что free_tier NOT NULL (вместо NULL 0) --->
<cfset var payable_metric_dimension_1 = (isNumeric(accum_metric_dimension_1))? round(max(accum_metric_dimension_1 - free_tier, 0),3):javaCast("null",0)/>
<cfset var cost_dimension_1 = precisionEvaluate("gaussianRound(payable_metric_dimension_1*gaussianRound(discounted_price, 2), 2)")/>
<cfset accum_cost_svc = precisionEvaluate("accum_cost_svc + cost_dimension_1")/>
<cfset "temp.component.metric"=#(accum_metric_dimension_1 NEQ 0 AND isNumeric(accum_metric_dimension_1))? round(accum_metric_dimension_1,3):javaCast("null",0)#/>
<cfset "temp.component.metricPayable"=#payable_metric_dimension_1#/>
<cfset "temp.component.cost"=#cost_dimension_1#/>
<cfset arrayAppend(temp.service.components, temp.component)/>
</cfoutput><!--- serviceStart (component version in specification, that is svc version joined down to the component) --->
<cfset "temp.service.cost"=#accum_cost_svc#/>
</cfoutput><!--- code (component) --->
<cfset arrayAppend(temp.period.services, temp.service)/>
</cfoutput><!--- svc --->
</cfoutput><!--- month --->
</cfoutput><!--- year --->
<cfset "temp.out.durationMs"=(getTickCount()-request.startTickCount)/>
<cfcontent
type="application/json"
/><cfoutput>#serializeJSON(out)#</cfoutput>
/><cfoutput>#serializeJSON(temp.out)#</cfoutput>
+168 -79
View File
@@ -8,9 +8,13 @@
<cfparam name="ATTRIBUTES.tenant_wz_index" type="string"/>
<cfset hours=dateDiff('h', ATTRIBUTES.dt_start, ATTRIBUTES.dt_finish)/> <!--- *** нужно для каждого фрагмента --->
<cfif hours LE 0>
<cfthrow message="Некорректный период" detail="Период должен быть больше 0"/>
</cfif>
<!---
*** ВНИМАНИЕ! Вычитать минимальный лимит нужно после агрегации!
*** ВНИМАНИЕ! Вычитать минимальный бесплатный лимит (free tier) нужно после агрегации!
Исходная последовательность расчетов сначала делает агрегацию по месяцу, при расчете по дням нужно менять порядок
--->
<!---
@@ -21,13 +25,12 @@
<!--- *** Проверить, что у нас не биллится закрытый период (закрытый следующим) - возможно, с учетом line_key
- насколько вижу, прошлый период закрывается, причем концы отрезки закрытые (конец включается). Считаем, что этой проблемы нет
--->
<!--- QoQ does not support join syntax, and of course outer joins, at least of Lucee 5.4--->
<!--- а внешние джойны хорошо подсвечивают ошибки, особенно помогает full --->
<!--- а внешние джойны хорошо подсвечивают ошибки в данных, особенно помогает full --->
<cfmodule template="crm_contract.cfm" wz="WZ01348" out="qSpecification"/><!--- WZ01200 --->
<cfmodule template="crm_contract.cfm" wz="WZ#numberFormat(ATTRIBUTES.tenant_wz_index,'00000')#" out="qSpecification"/>
<cfif (ATTRIBUTES.debug)>
qSpecification (from CRM)
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpecification#/>
<cfflush/>
@@ -77,41 +80,55 @@
-- бесплатный лимит есть только для S3 HOT (в месяц в единицах измерения) --->
<!--- здесь нужно организовать пересечение версионной спецификации с анализируемым диапазоном дат --->
<cfquery name="qSpec" dbtype="query">
select s.catalogName as svc, s.component, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, f.free_tier
from qSpecification s, qFreeTier f
WHERE s.fullCode=f.code
<!--- ******************************** здесь нужно организовать пересечение версионной спецификации с анализируемым диапазоном дат --->
<!--- Проверка: включаем дату смены цены в диапазон и смотрим --->
<!--- Важно: если в периоде было 2+ цены, то строки должны быть замножены, а цена за период перестает иметь смысл (не показывать же среднее арифметическое, это неправильно) --->
<!--- Left Outer Join with Free Tier --->
<cfquery name="qSpec" dbtype="query">
select s.catalogName as svc, s.component, s.serviceCode as service_code, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, f.free_tier
from qSpecification s, qFreeTier f
WHERE s.fullCode=f.code
AND s.serviceType = 3
UNION
UNION
select s.catalogName as svc, s.component, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, 0
from qSpecification s
WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsqltype="cf_sql_varchar" list="true">)
</cfquery>
select s.catalogName as svc, s.component, s.serviceCode as service_code, s.fullCode as code, s.measureShort as unit
, s.priceWVat as price, (s.discount + s.serviceDiscount - s.discount*s.serviceDiscount/100) as discount
,s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, 0
from qSpecification s
WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsqltype="cf_sql_varchar" list="true">)
AND s.serviceType = 3
</cfquery>
<!--- <cfdump var=#qSpec# abort=true/> --->
<cfif (ATTRIBUTES.debug)>
qSpec (Left Outer Join with Free Tier)
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qSpec#/>
<cfflush/>
</cfif>
<!--- <cfloop from=#attributes.dt_start# to=#attributes.dt_finish# step="#CreateTimeSpan( 1, 0, 0, 0 )#" index="local.dt">
<cfdump var=#dateFormat(local.dt,"YYYY-MM-DD")#/>--->
<!--- *** Нужно еще реализовать в модели данных политики бесплатного минимума, сейчас пробито гвоздями --->
<!--- *** Нужно еще реализовать в модели данных (каталоге) политики бесплатного минимума (free tier), сейчас прибито гвоздями --->
<cfquery name="qComputing">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
case
when vdc.name like '%-v1cl1%' then 'iaas.ngc.i29'
when vdc.name like '%-v1cl2%' then 'iaas.ngc.i31'
when vdc.name like '%-v1cl4%' then 'iaas.ngc.a28'
when vdc.name like '%-v1cl6%' then 'iaas.ngc.i28'
else '['||vdc.name||']' end as code,
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl5%' THEN 'iaas.ngc.a45'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28' -- *** смапить все кластера
WHEN vdc.name LIKE '%-v1cl7%' THEN 'iaas.ngc.a40' -- *** смапить все кластера
ELSE '['||vdc.name||']' end as code,
sum(cpu_used/cpu_speed)/4. as core_h,
sum(mem_used)/1024/4. as gb_h,
capacity_resource.timestamp::date as dt
capacity_resource.timestamp::date as dt,
vdc.name as dimension_1,
null as dimension_2
from ngcloud_ru.capacity_resource
join ngcloud_ru.vdc on capacity_resource.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
@@ -123,19 +140,23 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
</cfquery>
<cfif (ATTRIBUTES.debug)>
qComputing
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qComputing#/>
<cfflush/>
</cfif>
<!--- переделать storage_profile_types с расшифровкой ssd, sata, можно прямо до каталожного кода --->
<!--- очень долго исполняется --->
<cfquery name="qStorage">
select 'WZ'||to_char(tenant.wzcode,'FM00000') as wz, vdc.name as vdc_name,
case
WHEN vdc.name LIKE '%-v1cl1%' THEN 'iaas.ngc.i29'
WHEN vdc.name LIKE '%-v1cl2%' THEN 'iaas.ngc.i31'
WHEN vdc.name LIKE '%-v1cl4%' THEN 'iaas.ngc.a28'
WHEN vdc.name LIKE '%-v1cl5%' THEN 'iaas.ngc.a45'
WHEN vdc.name LIKE '%-v1cl6%' THEN 'iaas.ngc.i28'
WHEN vdc.name LIKE '%-v1cl7%' THEN 'iaas.ngc.a40'
ELSE '['||vdc.name||']' END
|| CASE
WHEN storage_profile_types.name ILIKE '%-SSD%' THEN '.ssd-m'
@@ -144,9 +165,11 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
ELSE '.['|| storage_profile_types.name ||']' END
as code,
storage_profile_types.name, -- *** это не тип, а профайл
sum("limit")/1024./4 as gb_h_limit, -- 4 потому, что сбор раз в 15 минут
sum(used)/1024./4 as gb_h_used,
capacity_storage.timestamp::date as dt
sum("limit")/1024./4./720. as gb_m_limit, -- 4 потому, что сбор раз в 15 минут, а цена за УСЛОВНЫЙ ГБ-месяц (720 ГБ-часов)
sum(used)/1024./4./720. as gb_m_used,
capacity_storage.timestamp::date as dt,
vdc.name as dimension_1,
storage_profile_types.name as dimension_2
from ngcloud_ru.capacity_storage
join ngcloud_ru.vdc on capacity_storage.vdc_id=vdc.id
join ngcloud_ru.tenant on tenant.id=vdc.tenant_id
@@ -168,9 +191,9 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
select /* *** ниже дублирование кода агрегации - round ceil etc, обратить внимание при правке */
(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>) as vol_B
,ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>/1024/1024/1024) as vol_GB
,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>
<!--- ,greatest(ceil(sum(bucket_stat.usage_rgw_main_size_actual)/<cfqueryparam cfsqltype="cf_sql_integer" value=#hours#/>
/1024/1024/1024)-
case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/
case when bucket_stat.placement_id = 1 then 1 else 0 end, 0) as vol_GB_free_tier_subtracted /*считается не в точности так, как в функции*/ --->
,case
when bucket_stat.placement_id = 1 then 'paas.s3.ceph'
when bucket_stat.placement_id = 2 then 'paas.s3.cphc'
@@ -178,6 +201,8 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
--,bucket_stat.placement_id
,'WZ'||lpad(bucket_stat.owner, 5, '0') as wz -- *** здесь не число, а строка, поэтому другая формула обратной сборки WZ
,bucket_stat.timestamp_addition::date as dt
,bucket_info.name as dimension_1,
null as dimension_2
from s3billing.bucket_stat
join s3billing.placement ON s3billing.placement.id=bucket_stat.placement_id
join s3billing.bucket_info ON bucket_info.id=bucket_stat.bucket_id
@@ -185,11 +210,12 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
AND bucket_stat.owner = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND bucket_stat.timestamp_addition::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND bucket_stat.timestamp_addition::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by bucket_stat.owner, bucket_stat.placement_id, bucket_stat.timestamp_addition::date
order by bucket_stat.owner, bucket_stat.placement_id, bucket_stat.timestamp_addition::date;
group by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name, bucket_stat.timestamp_addition::date
order by bucket_stat.owner, bucket_stat.placement_id, bucket_info.name, bucket_stat.timestamp_addition::date;
</cfquery>
<cfif (ATTRIBUTES.debug)>
qS3Vol
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3Vol#/>
<cfflush/>
@@ -212,14 +238,14 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) as get_10k
,greatest(ceil(sum(
/*,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'get%'
OR category.name LIKE 'head%'
OR category.name LIKE 'options%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 10 ELSE 0 END, 0
) as get_10k_free_tier_subtracted
) as get_10k_free_tier_subtracted*/
,sum(
CASE WHEN
@@ -237,7 +263,7 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) as put_10k
,greatest(ceil(sum(
/*,greatest(ceil(sum(
CASE WHEN
(category.name LIKE 'put%'
OR category.name LIKE 'post%'
@@ -245,12 +271,12 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
OR category.name LIKE 'list%'
) THEN ops ELSE 0 END
)/10000) - CASE WHEN bucket_info.placement_id = 1 THEN 1 ELSE 0 END, 0
) as put_10k_free_tier_subtracted
) as put_10k_free_tier_subtracted*/
,(sum(bytes_sent)) as bytes_sent
,ceil(sum(bytes_sent)/1024/1024/1024) as bytes_sent_GB
,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0
) as bytes_sent_GB_free_tier_subtracted
/*,greatest(ceil(sum(bytes_sent)/1024/1024/1024) - CASE WHEN bucket_info.placement_id = 1 THEN 100 ELSE 0 END, 0
) as bytes_sent_GB_free_tier_subtracted */
,case
when bucket_info.placement_id = 1 then 'paas.s3.ceph'
when bucket_info.placement_id = 2 then 'paas.s3.cphc'
@@ -259,6 +285,8 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
--category.name, bucket_info.name
,'WZ'||lpad(user_info.name, 5, '0') as wz
,usage_bucket_by_user.time::date as dt
,bucket_info.name as dimension_1
,null as dimension_2
from s3billing.usage_bucket_by_user
join s3billing.user_info on usage_bucket_by_user.user_id=user_info.id
join s3billing.category on usage_bucket_by_user.category_id=category.id
@@ -268,11 +296,14 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
AND user_info.name = <cfqueryparam cfsqltype="cf_sql_varchar" value=#ATTRIBUTES.tenant_wz_index#/>
AND usage_bucket_by_user.time::date >= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_start#/>
AND usage_bucket_by_user.time::date <= <cfqueryparam cfsqltype="cf_sql_timestamp" value=#attributes.dt_finish#/>
group by user_info.name, bucket_info.placement_id, usage_bucket_by_user.time::date
order by user_info.name, bucket_info.placement_id, usage_bucket_by_user.time::date;
group by user_info.name, bucket_info.placement_id, bucket_info.name, usage_bucket_by_user.time::date
order by user_info.name, bucket_info.placement_id, bucket_info.name, usage_bucket_by_user.time::date;
</cfquery>
<!--- здесь метрики уже сгруппированы по дням. Умножать на цену еще рано, надо сильно позже --->
<cfif (ATTRIBUTES.debug)>
qS3OpsTrf
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qS3OpsTrf#/>
<cfflush/>
@@ -280,25 +311,55 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
<!--- собрали все метрики в один резалтсет --->
<cfquery name="qUnifiedMetric" dbType="query">
select dt, wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric from qComputing
union all
select dt, wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric from qComputing
union all
select dt, wz, code, gb_h_used as raw_metric, gb_h_used as metric from qStorage
union all
select dt, wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric from qS3Vol
union all
select dt, wz, code || '.get-m', get as raw_metric, get_10k as metric from qS3OpsTrf
union all
select dt, wz, code || '.put-m', put as raw_metric, put_10k as metric from qS3OpsTrf
union all
select dt, wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric from qS3OpsTrf
select dt, wz, code || '.vcpu-m' as code, core_h as raw_metric, core_h as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, NULL as dimension_2_name
from qComputing
order by dt, code
union all
select dt, wz, code || '.ram-m' as code, gb_h as raw_metric, gb_h as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, NULL as dimension_2_name
from qComputing
union all
select dt, wz, code, gb_m_used as raw_metric, gb_m_used as metric,
dimension_1, 'vdc' as dimension_1_name, dimension_2, 'storage policy' as dimension_2_name
from qStorage
union all
select dt, wz, code || '.vol-m', vol_b as raw_metric, vol_gb as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name
from qS3Vol
union all
select dt, wz, code || '.get-m', get as raw_metric, get_10k as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name
from qS3OpsTrf
union all
select dt, wz, code || '.put-m', put as raw_metric, put_10k as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name
from qS3OpsTrf
union all
select dt, wz, code || '.trf-m', bytes_sent as raw_metric, bytes_sent_gb as metric,
dimension_1, 'bucket' as dimension_1_name, dimension_2, NULL as dimension_2_name
from qS3OpsTrf
order by dt, code, dimension_1, dimension_2 <!--- /*dimension_*_name нужно только в качестве заголовка *** потом оптимизировать */ --->
</cfquery>
<!--- clean extra spaces to work around QoQ bug --->
<cfloop query="qUnifiedMetric">
<cfset qUnifiedMetric.dimension_1_name=trim(qUnifiedMetric.dimension_1_name)/>
<cfset qUnifiedMetric.dimension_2_name=trim(qUnifiedMetric.dimension_2_name)/>
<!--- *** возможно, и с другими полями придется сделать то же самое --->
</cfloop>
<!--- This is a known, long-standing quirk in ColdFusion's Query of Queries (QoQ) engine.When you perform a UNION ALL across queries where a character column has a dynamic or varying string length (like your concatenated m.dimension_1_name + 'x'), QoQ identifies the longest string in that column across all result sets. It then treats the column like a fixed-length CHAR database field and pads all shorter values with trailing spaces to match that maximum length. --->
<!--- лучше никогда не связываться с QoQ, но если все исполнять в исходной бд, это скорее мегаселект с большим числом CTE --->
<!--- надо было сразу делать в БД. Причем сводить в одну, чтобы избежать нескольких источников данных... можно было бы и так добиться, но все равно сводим в буферную базу --->
<!--- <cfdump var=#qUnifiedMetric#/> --->
<cfif (ATTRIBUTES.debug)>
qUnifiedMetric
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qUnifiedMetric#/>
<cfflush/>
@@ -318,15 +379,19 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
<!--- Вычитание Free Tier можно начинать с начала месяца - для этого можно сгруппировать по месяцам --->
<!--- Кажется, здесь еще рано агрегировать, если мы хотим OUTER JOIN --->
<!--- агрегировать вообще не надо в этом компоненте, а стоимость вычислять после агрегации --->
<cfquery name="qChargeDaily" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Артикул">s.code</d:field>
<d:field title="Артикул">m.code as m_code</d:field>
<d:field title="Код компонента">s.code</d:field>
<d:field title="Код метрики">m.code as m_code</d:field>
<d:field title="Код услуги">s.service_code as service_code</d:field>
<d:field title="Услуга">s.svc</d:field>
<d:field title="Компонент">s.component</d:field>
<d:field title="Ед.изм.">s.unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">s.price*(100-s.discount)/100 as discounted_price</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">
s.price*(100-s.discount)/100 as discounted_price
</d:field>
<d:field title="Цена с НДС" cfSqlType="CF_SQL_NUMERIC">s.price</d:field>
<d:field title="Скидка%" cfSqlType="CF_SQL_NUMERIC">s.discount</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">m.raw_metric</d:field>
@@ -337,54 +402,77 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
<d:field title="serviceCustomName">s.serviceCustomName</d:field>
<d:field title="dt" cfSqlType="CF_SQL_TIMESTAMP">m.dt</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">s.free_tier</d:field>
<d:field>m.dimension_1</d:field>
<d:field>m.dimension_1_name</d:field>
<d:field>m.dimension_2</d:field>
<d:field>m.dimension_2_name</d:field>
</d:field_set>
FROM qSpec as s, qUnifiedMetric as m
WHERE m.wz=<cfqueryparam cfsqltype="cf_sql_varchar" value=#request.auth.wz#/>
AND s.code=m.code
AND s.serviceStart <= m.dt
AND (m.dt <= s.serviceEnd OR s.serviceEnd IS NULL)
<!--- таким замысловатым способом мы реализуем FULL JOIN --->
UNION
SELECT NULL, m.code, NULL, NULL, NULL, NULL, NULL, NULL, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0
UNION ALL
SELECT NULL, m.code, NULL, NULL, NULL, NULL, 0,
NULL, NULL, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0, m.dimension_1, m.dimension_1_name, m.dimension_2, m.dimension_2_name
FROM qUnifiedMetric as m where m.code NOT IN (<cfqueryparam value="#ValueList(qSpec.code)#" cfsqltype="cf_sql_varchar" list="true">)
UNION
SELECT s.code, NULL, s.svc, s.component, s.unit, s.price*(100-s.discount)/100, s.price, s.discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0
UNION ALL
SELECT s.code, NULL, s.service_code, s.svc, s.component, s.unit, s.price*(100-s.discount)/100 as discounted_price,
s.price, s.discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0, NULL, NULL, NULL, NULL
FROM qSpec as s where s.code NOT IN (<cfqueryparam value="#listRemoveDuplicates(ValueList(qUnifiedMetric.code))#" cfsqltype="cf_sql_varchar" list="true">)
order by m.dt, s.code
order by dt, code, dimension_1, dimension_2
</cfquery>
<!--- диагностика ошибок в QoQ крайне невнятна и вводит в заблуждение --->
<!---<cfdump var=#qChargeDaily# abort=true/> --->
<cfif (ATTRIBUTES.debug)>
qChargeDaily
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeDaily#/>
<cfflush/>
</cfif>
<!--- Из-за детализации придется вынести вычитание free_tier на более поздний этап --->
<!--- Здесь агрегировано за месяц, но детализировано по измерениям --->
<cfquery name="qChargeMonthly" dbType="query">
SELECT
<d:field_set titleMapOut="titleMap" lengthOut="fieldCount">
<d:field title="Артикул по договору">code</d:field>
<d:field title="Артикул по системам">m_code</d:field>
<d:field title="Услуга по каталогу">svc</d:field><!--- *** добавить пользовательское --->
<d:field title="Код услуги">service_code</d:field>
<d:field title="Услуга по каталогу">svc</d:field>
<d:field title="serviceCustomName">serviceCustomName</d:field>
<d:field title="Компонент">component</d:field>
<d:field title="Ед.изм.">unit</d:field>
<d:field title="Цена &##8381; с НДС" cfSqlType="CF_SQL_NUMERIC">discounted_price</d:field>
<d:field title="Сырая метрика" cfSqlType="CF_SQL_NUMERIC">sum(raw_metric) as raw_metric</d:field>
<d:field title="Приведенный объем" cfSqlType="CF_SQL_NUMERIC">sum(metric) as metric</d:field>
<d:field title="Объем к оплате" cfSqlType="CF_SQL_NUMERIC">sum(metric) - free_tier as chargeable_metric</d:field>
<d:field title="Сумма к оплате" cfSqlType="CF_SQL_NUMERIC">(sum(metric) - free_tier)*discounted_price as charge</d:field>
<d:field title="serviceStart" cfSqlType="CF_SQL_TIMESTAMP">serviceStart</d:field>
<d:field title="serviceEnd" cfSqlType="CF_SQL_TIMESTAMP">serviceEnd</d:field>
<d:field title="dt_billed_from" cfSqlType="CF_SQL_TIMESTAMP">min(dt) as dt_billed_from</d:field>
<d:field title="dt_billed_to" cfSqlType="CF_SQL_TIMESTAMP">max(dt) as dt_billed_to</d:field>
<d:field title="serviceHash">serviceHash</d:field>
<d:field title="serviceCustomName">serviceCustomName</d:field>
<d:field title="free tier" cfSqlType="CF_SQL_NUMERIC">free_tier</d:field>
<d:field>dimension_1</d:field>
<d:field>dimension_1_name</d:field>
<d:field>dimension_2</d:field>
<d:field>dimension_2_name</d:field>
<d:field title="Год" cfSqlType="CF_SQL_INTEGER">year(dt) as year</d:field>
<d:field title="Месяц" cfSqlType="CF_SQL_INTEGER">month(dt) as month</d:field>
</d:field_set>
from qChargeDaily
group by code, m_code, svc, serviceCustomName, component, unit, discounted_price, serviceStart, ServiceEnd, serviceHash, free_tier, year(dt), month(dt)
order by year(dt), month(dt), code, m_code
group by m_code, code, svc, service_code, serviceCustomName, component, unit, discounted_price, serviceStart, ServiceEnd, serviceHash, free_tier, year(dt), month(dt),
dimension_1_name, dimension_1, dimension_2_name, dimension_2
order by year(dt), month(dt), m_code, code, serviceStart, dimension_1, dimension_2
</cfquery>
<!--- в связи с бедностью языка QoQ (нет CASE) грубо патчим резалтсет --->
<cfloop query="qChargeMonthly">
<!--- <cfloop query="qChargeMonthly">
<cfset qChargeMonthly.chargeable_metric = (chargeable_metric GT 0) ? chargeable_metric : 0/> <!--- имя в левой части должно быть квалифицировано запросом (резалтсетом) --->
<cfset qChargeMonthly.charge = (charge GT 0) ? charge : 0/>
</cfloop>
</cfloop> --->
<!--- тут могут быть подводные камни с копированием и переиспользованием объектов --->
@@ -402,14 +490,14 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
<!--- <cfdump var=#qChargeMonthly#/> --->
<!--- *** проверить краевые даты --->
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeMonthly#/>
<cfflush/>
</cfif>
<cfif (ATTRIBUTES.debug)>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<cfdump var=#qChargeMonthly#/>
<cfflush/>
</cfif>
<cfoutput>#getTickCount() - request.startTickCount#</cfoutput>
<!--- <cfflush/> --->
@@ -420,6 +508,7 @@ WHERE s.fullCode NOT IN (<cfqueryparam value="#ValueList(qFreeTier.code)#" cfsql
<cfset report.qS3VolAge = qS3VolAge/>
<cfset report.qS3OpsTrfAge = qS3OpsTrfAge/>
<cfset report.qCharge = qChargeMonthly/>
<cfset report.qFreeTier = qFreeTier/><!--- мы не применяем скидку на уровне детализации, а только после агрегации. На самом деле правильно было бы агрегировать натуральные метрики, а умножать на цену уже агрегаты --->
<cfset report.chargeTitleMap = titleMap/>
<cfset report.chargeFieldCount = fieldCount/>
+9 -4
View File
@@ -14,9 +14,12 @@
</cfhttp>
<cfset local={}/>
<cfset company=deserializeJson(res.fileContent)/>
<!--- <cfdump var=#company#/> --->
<!--- --->
<cfset safeJsonString = REReplace(res.fileContent, '(:\s*)(-?\d+\.\d{16,})', '$1"$2"', "ALL")/><!--- очень длинные числа завернуть в кавычки --->
<!--- <cfdump var=#safeJsonString#/> --->
<cfset company=deserializeJson(safeJsonString)/>
<!---<cfset company=deserializeJson(res.fileContent)/> --->
<!---<cfdump var=#company#/> --->
<!--- *** это не слишком надежно, некорректно представленный NULL (обыкновенно представляемый пустой строкой) может все сломать. Да и корректный --->
<!--- мартышкин труд собирать этот запрос из JSON , может, лучше было динамически --->
<cfset qComponent = queryNew("
@@ -47,7 +50,9 @@
,discount
,atomicCode
,fullCode
")/>
",
"varChar,varChar,varChar,varChar,integer,varChar,integer,varChar,varChar,varChar,timestamp,timestamp,decimal,decimal,varChar,integer,varChar,varChar,varChar,decimal,decimal,decimal,varChar,varChar")/>
<!--- если не указывать типы, он пытается их определить сам, потом проблемы с union и арифметикой --->
<!--- this.nullSupport = true in Application.cfc ... no way --->
<cfloop array=#company.contracts# item="local.contract"><!--- имя массива писать в ##, иначе думает, что это строковый литерал --->
+22
View File
@@ -0,0 +1,22 @@
SQL SELECT
s.code,m.code as m_code,s.service_code as service_code,s.svc,s.component,s.unit,
s.price*(100-s.discount)/100 as discounted_price
,s.price,s.discount,m.raw_metric,m.metric,s.serviceStart,s.serviceEnd,s.serviceHash,s.serviceCustomName,m.dt,s.free_tier,m.dimension_1,m.dimension_1_name ||'x' as dimension_1_name,m.dimension_2,m.dimension_2_name
FROM qSpec as s, qUnifiedMetric as m
WHERE m.wz='62'
AND s.code=m.code
AND s.serviceStart <= m.dt
AND (m.dt <= s.serviceEnd OR s.serviceEnd IS NULL)
UNION ALL
SELECT NULL, m.code, NULL, NULL, NULL, NULL, 0,
NULL, NULL, m.raw_metric, m.metric, NULL, NULL, NULL, NULL, m.dt, 0, m.dimension_1, m.dimension_1_name ||'y', m.dimension_2, m.dimension_2_name
FROM qUnifiedMetric as m where m.code NOT IN ('5','6')
UNION ALL
SELECT s.code, NULL, s.service_code, s.svc, s.component, s.unit, s.price*(100-s.discount)/100 as discounted_price,
s.price, s.discount, NULL, NULL, s.serviceStart, s.serviceEnd, s.serviceHash, s.serviceCustomName, NULL, 0, NULL, NULL, NULL, NULL
FROM qSpec as s where s.code NOT IN ('5','6')
order by m.dt, m.code, dimension_1, dimension_2
+135 -54
View File
@@ -6,15 +6,22 @@
</cfsilent>
<m:prepare_ls entity="payg" accessObject="" pageInfoOut="pageInfo" trackOut="tr"/>
<!--- <cfdump var=#form#/> --->
<cfparam name="dt_finish" type="date" default=#createDateTime(year(Now()),month(Now()),1,0,0,0)#/>
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,dt_finish)#/>
<cfset hours=dateDiff('h',dt_start,dt_finish)/>
<cfparam name="dt_finish" type="date" default=#dateAdd('d',-1,createDateTime(year(Now()),month(Now()),1,0,0,0))#/>
<cfparam name="dt_start" type="date" default=#dateAdd('m',-1,createDateTime(year(Now()),month(Now()),1,0,0,0))#/>
<!--- <cfparam name="dt_finish" default=#dateAdd('d',-1,createDateTime(year(Now()),month(Now()),1,0,0,0))#/>
<cfparam name="dt_start" default=#dateAdd('m',-1,createDateTime(year(Now()),month(Now()),1,0,0,0))#/> --->
<cfset hours=dateDiff('h', dt_start, dt_finish)/>
<!--- <cfdump var=#variables#/> --->
<cfset tenant_wz_index=val(mid(request.auth.wz,3,5))/><!--- cut off WZ --->
<cfif isDefined("output_xls")><cfmodule template="mod/payg.cfm"
<cfif isDefined("output_xls")><cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
@@ -22,7 +29,7 @@
debug=#isDefined("DEBUG")#
/><layout:xml qRead=#report.qCharge# titleMap=#report.chargeTitleMap# filename="#pageInfo.entity#.xml"/>
<cfabort/>
</cfif><cfif isDefined("output_json")><cfmodule template="mod/payg.cfm"
</cfif><cfif isDefined("output_json")><cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
@@ -42,38 +49,45 @@
</layout:attribute>
</layout:page>
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) должен выполняться в бухгалтерской программе.
<!--- Внимание! округление вверх при расчете метрик get, put S3. Расчетные суммы могут содержать погрешности округления, поэтому окончательный расчет стоимости (количество * цену) выполняeтся в бухгалтерской программе.
<b>Если у клиента больше 1 действующего допника с услугами PAYG, то данные в отчете могут быть замножены, в этом случае считать вручную</b> --->
<cfoutput>
<form method="post" action="">
Период с <input type="text" name="dt_start" value="#dateFormat(dt_start,'YYYY-MM-DD')#"/>
по <input type="text" name="dt_finish" value="#dateFormat(dt_finish,'YYYY-MM-DD')#"/>
<input type="submit" style="cursor:pointer;"/>
<input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/><!--- ********* --->
<form method="post" action="#request.thisPage#" style="display:inline"><!--- не надо делать action="" - параметр, затесавшийся в URL будет затирать параметр формы, и заметить это трудно --->
Период с <input type="text" name="dt_start" value="#dateFormat(dt_start,'YYYY-MM-DD')#"/>
по <input type="text" name="dt_finish" value="#dateFormat(dt_finish,'YYYY-MM-DD')#"/>
<input type="submit" style="cursor:pointer;"/>
<input type="submit" name="DEBUG" value="DEBUG" style="cursor:pointer;"/><!--- ********* --->
</form>
<cfset month_start = createDate(year(now()), month(now()), 1)/>
<a href="?dt_start=#dateFormat(dateAdd('m', -1, month_start),'YYYY-MM-DD')#&dt_finish=#dateFormat(dateAdd('d', -1, month_start),'YYYY-MM-DD')#">прошлый месяц</a>
<br/>
<!--- <cfflush/> --->
<cfmodule template="mod/payg.cfm"
<cfmodule template="mod/compute_payg.cfm"
dt_finish=#dt_finish#
dt_start=#dt_start#
tenant_wz_index=#tenant_wz_index#
output="report"
debug=#isDefined("DEBUG")#
debug=#structKeyExists(Form,"DEBUG")#
/>
Часов в периоде: <b>#hours#</b> Строк в отчете: <b>#report.qCharge.recordCount#</b><br/>
Актуальность данных:
VCD Computing: <b>#dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:MM:SS')#</b>
VCD Storage: <b>#dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:MM:SS')#</b>
S3 хранение: <b>#dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:MM:SS')#</b>
S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:MM:SS')#</b>
Цены GPL<br/>
VCD Computing: <b<cfif datediff("h",report.qComputingAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qComputingAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qComputingAge.dt_load,'HH:MM:SS')#
</b>
VCD Storage: <b<cfif datediff("h",report.qStorageAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qStorageAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qStorageAge.dt_load,'HH:MM:SS')#</b>
S3 хранение: <b<cfif datediff("h",report.qS3VolAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qS3VolAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3VolAge.dt_load,'HH:MM:SS')#
</b>
S3 операции и трафик: <b<cfif datediff("h",report.qS3OpsTrfAge.dt_load, Now()) GT 24> class="err"</cfif>>
#dateFormat(report.qS3OpsTrfAge.dt_load,'YYYY-MM-DD')# #timeFormat(report.qS3OpsTrfAge.dt_load,'HH:MM:SS')#
</b>
<br/>
<a href="#request.thisPage#?output_xls" title="экспорт в Excel" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/xls.gif" style="vertical-align:text-bottom;"/></a>
<a href="#request.thisPage#?output_json" title="экспорт в json" style="margin-left:.5em; height:100%;" target="_blank"><img src="img/json.svg" style="vertical-align:text-bottom;" width="13" height="13"/></a>
@@ -81,19 +95,21 @@ S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,
<table class="worktable">
<thead>
<td width="1%"></td>
<!--- <th width="3%">WZ</th> --->
<th width="7%">Артикул</th>
<th width="7%" colspan="2">Артикул</th>
<th width="3%">С</td>
<th width="3%">По</td>
<th width="3%">Кол-во (метрика)</td>
<th width="3%">Free Tier</td>
<th width="7%">Оплачиваемый объем</th>
<th width="3%">Ед.изм.</th>
<th width="3%">Цена &#8381; с НДС</th>
<th width="3%">Стоимость &#8381; с НДС</th>
<th width="3%">&nbsp;&nbsp;&nbsp;</th>
<th width="10%">Услуга</th>
<th width="10%">Компонент</th>
<th width="3%">Ед.изм.</th>
<!--- <td width="10%">Клиентс. назв.</td> --->
<!--- <td width="3%">GPL с НДС</td>
@@ -112,63 +128,128 @@ S3 операции и трафик: <b>#dateFormat(report.qS3OpsTrfAge.dt_load,
</thead>
<!--- *** Обнаружена засада: возможно, группировка (сортировка) отображаемого вывода нужна по размерности детализации, а не по коду
Но: агрегация скорее нужна по коду
Видимо, придется на фронте перетасовывать?
--->
<!--- Еще засада: если я делегирую агрегацию на фронт, мне нужно и вычет free_tier делегировать на фронт (помня еще, что он на месяц, а не на произвольный период - наверно, можно его вычитать всегда, а больше месяца не агрегировать... или умножать на число полных месяцев) --->
<!--- видимо, Free Tier и единицу измерения нужно брать и для строк, которые не найдены в договоре
(еше можно было бы туда для полноты добавлять GPL цену, но для этого нужен каталог)
Добавить реквизиты договора? --->
<cfoutput query="report.qCharge" group="year">
<cfoutput group="month">
<tr><td colspan="99"><b>#year#-#numberFormat(month,"00")#</b></td></tr>
<tr><td colspan="99"><cfif month GT 0><b>#year#-#numberFormat(month,"00")#</b></cfif></td></tr>
<cfoutput group="svc">
<cfset var acc = 0/>
<cfset var accum_cost_svc = 0/>
<cfset var accum_metric_svc = 0/>
<tr>
<td bgcolor="##ccc"></td>
<td bgcolor="##ccc" colspan="9">
<b>#svc#</b>
<td bgcolor="##ccc" colspan="12">
#svc# <b>#service_code#</b>
<cfif len(serviceCustomName)>(#serviceCustomName#)</cfif>
<cfif serviceHash EQ "">(не найдено в договоре)</cfif>
</td>
</tr>
<cfoutput>
<cfset acc += isNumeric(charge) ? charge : 0/>
<tr>
<td>#currentRow#</a></td>
<td>#code# <cfif code EQ "">(#m_code#)</cfif></td>
<td class="r">#metric#</td>
<td class="r" style="font-size:120%; padding:0 1em;">#chargeable_metric#</td>
<cfoutput group="code">
<cfoutput group="serviceStart"> <!--- может быть, для ясности надо в каждой строке писать точный период --->
<cfset accum_metric_svc += isNumeric(metric) ? metric : 0/><!--- агрегация по метрике возможна только на уровне компонента --->
<tr>
<td></td>
<td colspan="10">#code# <cfif code EQ "">(#m_code#)</cfif></td>
<td>#svc#</td>
<td>#component# </td>
</tr>
<td class="r">#discounted_price#</td>
<td class="r">#numberFormat(charge,".00")#</td>
<cfset accum_metric_dimension_1 = 0/>
<td><!--- #acc# ---></td>
<cfoutput group="dimension_1">
<cfset accum_metric_dimension_1 += isNumeric(metric) ? metric : 0/>
<tr class="i">
<td>#currentRow#</td>
<td>#dimension_1_name#</td>
<td>#dimension_1#</td>
<td class="c">#dateFormat(dt_billed_from,"YYYY-MM-DD")#</td>
<td class="c">#dateFormat(dt_billed_to,"YYYY-MM-DD")#</td>
<td class="r"><cfif metric NEQ 0 AND isNumeric(metric)>#round(metric,3)#</cfif></td>
<td><!--- free tier ---></td>
<td class="r" style="font-size:120%; padding:0 1em;"></td>
<td class="c">#unit#</td>
<td class="r">#gaussianRound(discounted_price,2)#</td>
<td class="r"><!--- здесь не вычисляется стоимость, потому что Free Tier вычитается после агрегации (скидочные политики реализовывать будет непросто) ---></td>
<td></td>
<td></td>
</tr>
</cfoutput>
<td>#svc#</td>
<td>#component#</td>
<td class="c">#unit#</td>
<!--- <td>#user_description#</td> --->
<cfset cost_dimension_1 = precisionEvaluate("gaussianRound(max(accum_metric_dimension_1 - free_tier, 0)*gaussianRound(discounted_price, 2), 2)")/>
<!--- <td class="r">#price#</td>
<td class="r">#discount#</td> --->
<tr class="b">
<td></td>
<td colspan="4">Итого #code# <cfif code EQ "">(#m_code#)</cfif>:</td>
<td class="r"><cfif accum_metric_dimension_1 NEQ 0 AND isNumeric(accum_metric_dimension_1)>#round(accum_metric_dimension_1,3)#</cfif></td>
<td class="r">#free_tier#</td>
<td class="r" style="font-size:120%; padding:0 1em;"><cfif accum_metric_dimension_1 NEQ 0 AND isNumeric(accum_metric_dimension_1)>#round(max(accum_metric_dimension_1 - free_tier, 0),3)#</cfif></td>
<td class="c">#unit#</td>
<td class="r">#gaussianRound(discounted_price,2)#</td><!--- если цена менялась в периоде, неправильно показыввать последнюю - но мы не тестировали с изменением в периоде --->
<!--- непонятно, как вычитать Free Tier для неполных месяцев. Если с 15 по 15 - вычитать 2 раза, вычитать (каким-то сложным образом) 1 раз или ни разу --->
<!--- видимо, нужно сформировать тест кейсы на неполные месяцы. Где взять биллинговые данные для этого ВПР --->
<td class="r">#cost_dimension_1#</td>
<cfset accum_cost_svc = precisionEvaluate("accum_cost_svc + cost_dimension_1")/><!--- простое сложение дает ошибку округления и лишние знаки, но и precisionEvaluate не выглядит слишком надежным --->
<td>#svc# <!--- #service_code# ---></td>
<td>#component# </td>
</tr>
</tr>
</cfoutput>
</cfoutput><!--- serviceStart --->
</cfoutput><!--- code --->
<tr>
<td bgcolor="##eee" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="4" class="r" style="border-bottom:2px solid gray;"><b>Итого</b> &nbsp;&nbsp;&nbsp; #svc#:</td>
<td bgcolor="##eee" colspan="1" class="r b" style="border-bottom:2px solid gray;">#NumFmt(acc,2)#</td>
<td colspan="12" style="border-bottom:2px solid gray;"></td>
<td bgcolor="##eee" colspan="9" class="r" style="border-bottom:2px solid gray;">
<b>Итого </b> &nbsp;&nbsp;&nbsp;
<cfif len(service_code)>
#svc# <b>#service_code#</b>
<cfelse>
(#m_code#)
</cfif>
<cfif month GT 0>за #year#-#numberFormat(month,"00")#</cfif>:
</td>
<td bgcolor="##eee" class="r b" style="border-bottom:2px solid gray;">#accum_cost_svc#</td>
<td bgcolor="##eee" colspan="99" style="border-bottom:2px solid gray;"></td>
</tr >
</cfoutput>
<tr><td colspan="99">&nbsp;</td></tr>
</cfoutput>
</cfoutput>
</table>
<!--- <cfdump var=#qCharge#/> --->
<!---<cfdump var=#report.qCharge#/> --->
<!--- <cfdump var=#this.datasources[request.ds].connectionString#/> --->
<layout:page section="footer"/>
<!--- select * from s3billing.billing_per_user_fix_pl('2025-05-01 00:00:00','2025-06-01 00:00:00','1395'); --->
<cfscript>
function gaussianRound(numeric number, numeric precision = 0) {
var factor = 10 ^ precision;
var scaledNumber = number * factor;
var absScaled = abs(scaledNumber);
var sign = sgn(scaledNumber);
var floored = int(absScaled);
// If it's not ending in exactly .5, use standard rounding or precision scaling
if (absScaled - floored != 0.5) {
return round(number * factor) / factor;
}
// If it is exactly .5, round towards the nearest even number
if (floored % 2 != 0) {
// Nearest even is UP
return (floored + 1) * sign / factor;
} else {
// Nearest even is DOWN
return floored * sign / factor;
}
}
</cfscript>