diff --git a/src/main/java/org/folio/eusage/reports/api/CostPerUse.java b/src/main/java/org/folio/eusage/reports/api/CostPerUse.java index 486c47e0..ee2cc825 100644 --- a/src/main/java/org/folio/eusage/reports/api/CostPerUse.java +++ b/src/main/java/org/folio/eusage/reports/api/CostPerUse.java @@ -155,9 +155,9 @@ static JsonObject titlesToJsonObject(RowSet rowSet, Periods usePeriods) { item.put("poLineIDs", poLineIDs); JsonArray invoiceNumbers = new JsonArray(); - String invoiceNumber = row.getString("invoicenumber"); - if (invoiceNumber != null) { - invoiceNumbers.add(invoiceNumber); + String invoicenumbers = row.getString("invoicenumbers"); + if (invoicenumbers != null) { + invoiceNumbers.add(invoicenumbers); } item.put("invoiceNumbers", invoiceNumbers); if (usageDateRange != null) { diff --git a/src/main/java/org/folio/eusage/reports/api/EusageReportsApi.java b/src/main/java/org/folio/eusage/reports/api/EusageReportsApi.java index 4ab73010..45d0e079 100644 --- a/src/main/java/org/folio/eusage/reports/api/EusageReportsApi.java +++ b/src/main/java/org/folio/eusage/reports/api/EusageReportsApi.java @@ -81,6 +81,10 @@ static String agreementEntriesTable(TenantPgPool pool) { return pool.getSchema() + ".agreement_entries"; } + static String subscriptionCostsView(TenantPgPool pool) { + return pool.getSchema() + ".aggregated_subscription_costs"; + } + static String statusTable(TenantPgPool pool) { return pool.getSchema() + ".status"; } @@ -1240,7 +1244,7 @@ Future parsePoLine(JsonObject poLine, RoutingContext ctx) { result.put("fiscalYear", fiscalYears); result.put("invoicedPeriods", invoicedPeriods); JsonArray invoiceNumbers = new JsonArray(); - result.put("invoiceNumber", invoiceNumbers); + result.put("invoiceNumbers", invoiceNumbers); UUID poLineId = UUID.fromString(poLine.getString("poLineId")); return lookupOrderLine(poLineId, ctx).compose(orderLine -> { result.put("poLineNumber", orderLine.getString("poLineNumber")); @@ -1382,7 +1386,7 @@ private Future populateParsedPoLine(TenantPgPool pool, SqlConnection con, JsonArray fiscalYears = poResult.getJsonArray("fiscalYear"); JsonArray subscriptionPeriods = poResult.getJsonArray("subscriptionPeriods"); JsonArray invoicedPeriods = poResult.getJsonArray("invoicedPeriods"); - JsonArray invoiceNumbers = poResult.getJsonArray("invoiceNumber"); + JsonArray invoiceNumbers = poResult.getJsonArray("invoiceNumbers"); if (subscriptionPeriods.isEmpty()) { return insertAgreementLine(pool, con, agreementId, agreementLineId, coverageDateRanges, type, kbTitleId, kbPackageId, poLineId, poResult, @@ -1730,10 +1734,10 @@ static Future> getTitlesCost(TenantPgPool pool, Boolean isJournal, b + " kbPackageId, kbPackageName, printISSN, onlineISSN, ISBN," + " NULL AS publicationDate, NULL AS usageDateRange," + " NULL AS uniqueAccessCount, NULL AS totalAccessCount, TRUE AS openAccess," - + " orderType, poLineNumber, invoiceNumber," + + " orderType, poLineNumber, invoiceNumbers," + " fiscalYearRange, subscriptionDateRange," + " encumberedCost, invoicedCost" - + " FROM " + agreementEntriesTable(pool) + + " FROM " + subscriptionCostsView(pool) + " AS agreement_entries" + " LEFT JOIN " + packageEntriesTable(pool) + " USING (kbPackageId)" + " JOIN " + titleEntriesTable(pool) + " ON" + " title_entries.kbTitleId = agreement_entries.kbTitleId OR" @@ -1745,10 +1749,10 @@ static Future> getTitlesCost(TenantPgPool pool, Boolean isJournal, b + " title_entries.kbTitleId AS kbId, kbTitleName AS title," + " kbPackageId, kbPackageName, printISSN, onlineISSN, ISBN," + " publicationDate, usageDateRange, uniqueAccessCount, totalAccessCount, openAccess," - + " orderType, poLineNumber, invoiceNumber," + + " orderType, poLineNumber, invoiceNumbers," + " fiscalYearRange, subscriptionDateRange," + " encumberedCost, invoicedCost" - + " FROM " + agreementEntriesTable(pool) + + " FROM " + subscriptionCostsView(pool) + " AS agreement_entries " + " LEFT JOIN " + packageEntriesTable(pool) + " USING (kbPackageId)" + " JOIN " + titleEntriesTable(pool) + " ON" + " title_entries.kbTitleId = agreement_entries.kbTitleId OR" @@ -1956,6 +1960,48 @@ public Future postInit(Vertx vertx, String tenant, JsonObject tenantAttrib + "id UUID PRIMARY KEY, " + "status json" + ")", + "DROP VIEW IF EXISTS " + subscriptionCostsView(pool), + "CREATE VIEW " + subscriptionCostsView(pool) + " AS\n" + + "SELECT kbtitleid,\n" + + " kbpackageid,\n" + + " type,\n" + + " agreementid,\n" + + " agreementlineid,\n" + + " coveragedateranges,\n" + + " ordertype,\n" + + " date_part('year', lower(subscriptiondaterange)) AS year,\n" + + " TRANSLATE(CAST(range_agg(subscriptiondaterange) AS TEXT),'{}','') " + + " AS subscriptiondaterange,\n" + + " TRANSLATE(CAST(range_agg(fiscalyearrange) AS TEXT),'{}','') " + + " AS fiscalyearrange,\n" + + " string_agg(DISTINCT polinenumber, '/') AS poLineNumber,\n" + + " string_agg(DISTINCT invoicenumber, '/') AS invoicenumbers,\n" + + " sum(invoicedcost) AS invoicedcost,\n" + + " sum(encumberedcost) AS encumberedcost\n" + // Uncertain if same invoice could be inserted into table multiple times. + // Ensuring uniqueness. + + "FROM (SELECT DISTINCT kbtitleid,\n" + + " kbpackageid,\n" + + " type,\n" + + " agreementid,\n" + + " agreementlineid,\n" + + " fiscalyearrange,\n" + + " subscriptiondaterange,\n" + + " coveragedateranges,\n" + + " ordertype,\n" + + " polinenumber,\n" + + " invoicenumber,\n" + + " invoicedcost,\n" + + " encumberedcost\n" + + " FROM " + agreementEntriesTable(pool) + ")\n" + + "GROUP BY kbtitleid,\n" + + " kbpackageid,\n" + + " type,\n" + + " agreementid,\n" + + " agreementlineid,\n" + + " year,\n" + + " coveragedateranges,\n" + + " ordertype", "CREATE OR REPLACE FUNCTION " + pool.getSchema() + ".floor_months(date, integer)" + " RETURNS date AS $$\n" + "-- floor_months(date, n) returns the start of the period date belongs to,\n" diff --git a/src/test/java/org/folio/eusage/reports/MainVerticleTest.java b/src/test/java/org/folio/eusage/reports/MainVerticleTest.java index 5113813e..4ce381dd 100644 --- a/src/test/java/org/folio/eusage/reports/MainVerticleTest.java +++ b/src/test/java/org/folio/eusage/reports/MainVerticleTest.java @@ -535,7 +535,7 @@ static void getInvoiceLines(RoutingContext ctx) { ctx.response().end("limit missing"); return; } - UUID poLineId = UUID.fromString(query.substring(10)); + UUID poLineId = UUID.fromString(query.substring(10,46)); JsonArray ar = new JsonArray(); for (int i = 0; i < poLineIds.length; i++) { diff --git a/src/test/java/org/folio/eusage/reports/api/EusageReportsApiTest.java b/src/test/java/org/folio/eusage/reports/api/EusageReportsApiTest.java index 36530e09..fb9a4770 100644 --- a/src/test/java/org/folio/eusage/reports/api/EusageReportsApiTest.java +++ b/src/test/java/org/folio/eusage/reports/api/EusageReportsApiTest.java @@ -206,16 +206,21 @@ public void useOverTimeUnknownFormat() { static String te31 = "3100000e-0000-4000-8000-000000000000"; static String te32 = "3200000e-0000-4000-8000-000000000000"; - private static Future> insertAgreement(String agreementId, String titleId, String packageId) { + private static Future> insertAgreement(String agreementId, String titleId, String packageId, JsonObject values) { return pool.preparedQuery("INSERT INTO " + agreementEntriesTable(pool) - + "(id, agreementId, kbTitleId, kbPackageId)" - + " VALUES ($1, $2, $3, $4)") - .execute(Tuple.of(UUID.randomUUID(), agreementId, titleId, packageId)); - } - - private static Future> updateAgreement(String agreementId, String set) { - return pool.preparedQuery("UPDATE " + agreementEntriesTable(pool) + " SET " + set - + " WHERE agreementId = $1").execute(Tuple.of(UUID.fromString(agreementId))); + + "(id, agreementId, kbTitleId, kbPackageId, " + + "orderType, poLineNumber, invoiceNumber, subscriptionDateRange, fiscalYearRange, coverageDateRanges, " + + "encumberedCost, invoicedCost)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)") + .execute(Tuple.of(UUID.randomUUID(), agreementId, titleId, packageId, + values.getString("orderType"), + values.getString("poLineNumber"), + values.getString("invoiceNumber"), + values.getString("subscriptionDateRange"), + values.getString("fiscalYearRange"), + values.getString("coverageDateRanges"), + values.getInteger("encumberedCost"), + values.getInteger("invoicedCost"))); } private static Future> insertPackageEntry(String packageId, String packageName, String titleId) { @@ -251,38 +256,88 @@ private static Future> insertTitleData(String titleEntryId, } private static Future loadSampleData() { - return insertAgreement(a1, t11, null) - .compose(x -> insertAgreement(a1, t12, null)) - .compose(x -> updateAgreement(a1, "orderType = 'Ongoing', poLineNumber = '[\"p1\"]', invoiceNumber = '[\"i1\"]'," - + " fiscalYearRange='[2020-01-01,2021-01-01)'," - + " coverageDateRanges='[1998-01-01,2020-01-01]'," - + " encumberedCost = 100, invoicedCost = 110" - )) - .compose(x -> insertAgreement(a2, t21, null)) - .compose(x -> insertAgreement(a2, t22, null)) - .compose(x -> insertAgreement(a2, t31, null)) - .compose(x -> insertAgreement(a2, t32, null)) - .compose(x -> insertAgreement(a2, t21, null)) // dup - .compose(x -> insertAgreement(a2, t22, null)) // dup - .compose(x -> insertAgreement(a2, t31, null)) // dup - .compose(x -> insertAgreement(a2, t32, null)) // dup - .compose(x -> updateAgreement(a2, "orderType = 'One-Time', poLineNumber = 'p2', invoiceNumber = 'i2'," - + " fiscalYearRange='[2020-01-01,2021-01-01)'," - + " coverageDateRanges='[1998-01-01,2021-01-01]'," - + " encumberedCost = 200, invoicedCost = 210" - )) - .compose(x -> insertAgreement(a3, null, p11)) - .compose(x -> updateAgreement(a3, "orderType = 'Ongoing', poLineNumber = 'p3', invoiceNumber = 'i3'," - + " subscriptionDateRange = '[2020-03-03, 2021-01-15]', fiscalYearRange='[2020-01-01,2021-01-01)'," - + " coverageDateRanges='[1998-01-01,2021-01-01]'," - + " encumberedCost = 300, invoicedCost = 310" - )) - .compose(x -> insertAgreement(a4, null, p11)) - .compose(x -> updateAgreement(a4, "orderType = 'Ongoing', poLineNumber = 'p3', invoiceNumber = 'i3'," - + " subscriptionDateRange = '[2020-05-01, 2021-01-01]'," - + " coverageDateRanges='[1998-01-01,2021-01-01]'," - + " encumberedCost = 300, invoicedCost = 310" - )) + return insertAgreement(a1, t11, null, + new JsonObject() + .put("orderType", "Ongoing") + .put("poLineNumber", "[\"t11-p1\")") + .put("invoiceNumber", "[\"i1\"]") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 100) + .put("invoicedCost", 110)) + .compose(x -> insertAgreement(a1, t12, null, + new JsonObject() + .put("orderType", "Ongoing") + .put("poLineNumber", "[\"t12-p1\")") + .put("invoiceNumber", "[\"t12-i1\"]") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 100) + .put("invoicedCost", 110))) + .compose(x -> insertAgreement(a2, t21, null, + new JsonObject() + .put("orderType", "One-Time") + .put("poLineNumber", "p2") + .put("invoiceNumber", "t21-i2") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 200) + .put("invoicedCost", 210))) + .compose(x -> insertAgreement(a2, t22, null, + new JsonObject() + .put("orderType", "One-Time") + .put("poLineNumber", "p2") + .put("invoiceNumber", "t22-i2") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 200) + .put("invoicedCost", 210))) + .compose(x -> insertAgreement(a2, t31, null, + new JsonObject() + .put("orderType", "One-Time") + .put("poLineNumber", "p2") + .put("invoiceNumber", "t31-i2") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 200) + .put("invoicedCost", 210))) + .compose(x -> insertAgreement(a2, t32, null, + new JsonObject() + .put("orderType", "One-Time") + .put("poLineNumber", "p2") + .put("invoiceNumber", "t32-i2") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 200) + .put("invoicedCost", 210))) + .compose(x -> insertAgreement(a2, t21, null, + new JsonObject() + .put("orderType", "One-Time") + .put("poLineNumber", "p2") + .put("invoiceNumber", "t21-i2") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2020-01-01]") + .put("encumberedCost", 200) + .put("invoicedCost", -100))) // credit note + .compose(x -> insertAgreement(a3, null, p11, + new JsonObject() + .put("orderType", "Ongoing") + .put("poLineNumber", "p3") + .put("invoiceNumber", "i3") + .put("subscriptionDateRange", "[2020-03-03, 2021-01-15]") + .put("fiscalYearRange", "[2020-01-01,2021-01-01)") + .put("coverageDateRanges", "[1998-01-01,2021-01-01]") + .put("encumberedCost", 0) + .put("invoicedCost", 310))) + .compose(x -> insertAgreement(a4, null, p11, + new JsonObject() + .put("orderType", "Ongoing") + .put("poLineNumber", "p3") + .put("invoiceNumber", "i3") + .put("subscriptionDateRange", "[2020-05-01, 2021-01-01]") + .put("coverageDateRanges", "[1998-01-01,2021-01-01]") + .put("encumberedCost", 300) + .put("invoicedCost", 310))) .compose(x -> insertPackageEntry(p11, "Package 11", t11)) .compose(x -> insertPackageEntry(p11, "Package 11", t12)) .compose(x -> insertTitleSerial(te11, t11, "Title 11", "1111-1111", "1111-2222", "journal")) @@ -322,7 +377,7 @@ public void useOverTime(TestContext context) { assertThat(json.getLong("uniqueItemRequestsTotal"), is(38L)); assertThat((List) json.getJsonArray("totalItemRequestsByPeriod").getList(), contains(22L, 34L)); assertThat((List) json.getJsonArray("uniqueItemRequestsByPeriod").getList(), contains(20L, 18L)); -assertThat(json.getJsonArray("items").size(), is(4)); + assertThat(json.getJsonArray("items").size(), is(4)); assertThat(json.getJsonArray("items").getJsonObject(0).encodePrettily(), is(new JsonObject() .put("kbId", "11000000-0000-4000-8000-000000000000") @@ -1261,9 +1316,9 @@ public void costPerUseWithRoutingContext2(TestContext context) { assertThat((List) json.getJsonArray("uniqueItemRequestsByPeriod").getList(), contains(0, 40, 2, 0, 0)); assertThat((List) json.getJsonArray("totalItemCostsPerRequestsByPeriod").getList(), - contains(null, 0.44, 8.75, null, null)); + contains(null, 0.33, 6.67, null, null)); assertThat((List) json.getJsonArray("uniqueItemCostsPerRequestsByPeriod").getList(), - contains(null, 0.88, 17.5, null, null)); + contains(null, 0.67, 13.33, null, null)); })); } @@ -1275,6 +1330,16 @@ public void costPerUseWithRoutingContext2NoOA(TestContext context) { when(routingContext.request().params().get("startDate")).thenReturn("2020-04"); when(routingContext.request().params().get("endDate")).thenReturn("2020-08"); when(routingContext.request().params().get("includeOA")).thenReturn("false"); + /* + Agreement id: "a2" + Period: 2020-04 - 2020-08 inclusive + Titles/costs t21: cost (year): 210, -100 (credit note) = 110. + Usage: 03: 0, 05: 40, 06: 1 + t22: cost (year) 210 but no usage. + t31: cost (year) 210 + Usage: 05: 40 + t32: cost (year) 210 but is OA + */ new EusageReportsApi(webClient).getCostPerUse(vertx, routingContext) .onComplete(context.asyncAssertSuccess(x -> { ArgumentCaptor body = ArgumentCaptor.forClass(String.class); @@ -1288,10 +1353,12 @@ public void costPerUseWithRoutingContext2NoOA(TestContext context) { contains(0, 80, 0, 0, 0)); assertThat((List) json.getJsonArray("uniqueItemRequestsByPeriod").getList(), contains(0, 40, 0, 0, 0)); + // No-OA items with usage: 2. Cost (+210-100+210)/12=26.66. Per request: 26.66/80=0.33 assertThat((List) json.getJsonArray("totalItemCostsPerRequestsByPeriod").getList(), - contains(null, 0.44, null, null, null)); + contains(null, 0.33, null, null, null)); + // No-OA items with usage: 2. Cost (+210-100+210)/12=26.66. Per uniq request: 26.66/40=0.67 assertThat((List) json.getJsonArray("uniqueItemCostsPerRequestsByPeriod").getList(), - contains(null, 0.88, null, null, null)); + contains(null, 0.67, null, null, null)); })); } @@ -1561,16 +1628,24 @@ public void costPerFormatAllCsv(TestContext context) { context.assertEquals("p2", records.get(2).get(7)); context.assertEquals("p2", records.get(3).get(7)); context.assertEquals("Invoice number", header.get(8)); - context.assertEquals("i2", records.get(2).get(8)); - context.assertEquals("i2", records.get(3).get(8)); + context.assertEquals("t21-i2", records.get(2).get(8)); + context.assertEquals("t22-i2", records.get(3).get(8)); context.assertEquals("Cost per request - total", header.get(17)); - context.assertEquals("1.67", totals.get(17)); - context.assertEquals("0.83", records.get(2).get(17)); + context.assertEquals("1.47", totals.get(17)); + /* Title 21 + Cost: 210 - 100 = 110 = 9.16/month. Two months: 18.33 + Usage: 05: 40 06: 2 = 42 + Cost per request: 18.33/42 = 0.44 + */ + context.assertEquals("0.44", records.get(2).get(17)); context.assertEquals("0.88", records.get(4).get(17)); context.assertEquals("17.5", records.get(5).get(17)); context.assertEquals("Cost per request - unique", header.get(18)); - context.assertEquals("3.33", totals.get(18)); - context.assertEquals("1.67", records.get(2).get(18)); + context.assertEquals("2.94", totals.get(18)); + /* + Title 21, unique requests 21, cost/req 18.33/21 + */ + context.assertEquals("0.87", records.get(2).get(18)); context.assertEquals("1.75", records.get(4).get(18)); context.assertEquals("35.0", records.get(5).get(18)); } catch (IOException e) { @@ -1676,26 +1751,26 @@ public void costPerUseFormatJournalJson(TestContext context) { assertThat((List) json.getJsonArray("titleCountByPeriod").getList(), contains(1, 1)); assertThat((List) json.getJsonArray("totalItemCostsPerRequestsByPeriod").getList(), - contains(0.44, 8.75)); + contains(0.23, 4.58)); assertThat((List) json.getJsonArray("uniqueItemCostsPerRequestsByPeriod").getList(), - contains(0.88, 17.5)); - assertThat(json.getDouble("amountPaidTotal"), is(70.0)); - assertThat(json.getDouble("amountEncumberedTotal"), is(66.67)); + contains(0.46, 9.17)); + assertThat(json.getDouble("amountPaidTotal"), is(53.33)); + assertThat(json.getDouble("amountEncumberedTotal"), is(100.00)); assertThat(json.getJsonArray("items").size(), is(2)); assertThat(json.getJsonArray("items").getJsonObject(0).getString("kbId"), is(t21)); assertThat(json.getJsonArray("items").getJsonObject(0).getJsonArray("poLineIDs"), is(new JsonArray().add("p2"))); - assertThat(json.getJsonArray("items").getJsonObject(0).getJsonArray("invoiceNumbers"), is(new JsonArray().add("i2"))); + assertThat(json.getJsonArray("items").getJsonObject(0).getJsonArray("invoiceNumbers"), is(new JsonArray().add("t21-i2"))); assertThat(json.getJsonArray("items").getJsonObject(0).getString("fiscalDateStart"), is("2020-01-01")); assertThat(json.getJsonArray("items").getJsonObject(0).getString("fiscalDateEnd"), is("2020-12-31")); assertThat(json.getJsonArray("items").getJsonObject(0).getLong("totalItemRequests"), is(42L)); assertThat(json.getJsonArray("items").getJsonObject(0).getLong("uniqueItemRequests"), is(21L)); - assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("amountEncumbered"), is(33.33)); - assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("amountPaid"), is(35.0)); - assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("costPerTotalRequest"), is(0.83)); - assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("costPerUniqueRequest"), is(1.67)); + assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("amountEncumbered"), is(66.67)); + assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("amountPaid"), is(18.33)); + assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("costPerTotalRequest"), is(0.44)); + assertThat(json.getJsonArray("items").getJsonObject(0).getDouble("costPerUniqueRequest"), is(0.87)); assertThat(json.getJsonArray("items").getJsonObject(1).getString("kbId"), is(t22)); assertThat(json.getJsonArray("items").getJsonObject(1).getJsonArray("poLineIDs"), is(new JsonArray().add("p2"))); - assertThat(json.getJsonArray("items").getJsonObject(1).getJsonArray("invoiceNumbers"), is(new JsonArray().add("i2"))); + assertThat(json.getJsonArray("items").getJsonObject(1).getJsonArray("invoiceNumbers"), is(new JsonArray().add("t22-i2"))); assertThat(json.getJsonArray("items").getJsonObject(1).getString("fiscalDateStart"), is("2020-01-01")); assertThat(json.getJsonArray("items").getJsonObject(1).getString("fiscalDateEnd"), is("2020-12-31")); assertThat(json.getJsonArray("items").getJsonObject(1).getDouble("amountEncumbered"), is(33.33));