diff --git a/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphTypes.res b/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphTypes.res index fe7c8cfec..4ef4385e5 100644 --- a/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphTypes.res +++ b/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphTypes.res @@ -3,6 +3,25 @@ type options = {dataLabels: temp} type point = {sum: string, id: string, options: options} type nodeFormatter = {point: point} +type tooltipType = Node | Link + +type fromNode = {options: options} + +type toNode = {options: options} + +type pointFormat = { + sum: string, + id: string, + options: options, + color: string, + formatPrefix: string, + to: string, + from: string, + fromNode: fromNode, + toNode: toNode, +} +type pointFormatter = {point: pointFormat, key: string} + external asTooltipPointFormatter: Js_OO.Callback.arity1<'a> => nodeFormatter => string = "%identity" type enabled = {enabled: bool} @@ -41,6 +60,7 @@ type style = { fontWeight: fontWeight, fontSize: fontSize, color: color, + fontFamily: string, } type dataLabels = { style: style, @@ -56,6 +76,19 @@ type chart = { spacingLeft: int, spacingRight: int, } +type tooltip = { + style: style, + enabled: bool, + useHTML: bool, + formatter: nodeFormatter => string, + crosshairs: bool, + shadow: bool, + shape: string, + backgroundColor: string, + borderColor: string, + borderWidth: float, +} + type series = { exporting: exporting, credits: credits, @@ -70,7 +103,13 @@ type series = { } type title = {text: string} -type sankeyGraphOptions = {title: title, series: array, chart: chart, credits: credits} +type sankeyGraphOptions = { + title: title, + series: array, + chart: chart, + credits: credits, + tooltip: tooltip, +} type sankeyPayload = { title: title, diff --git a/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphUtils.res b/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphUtils.res index 33ebaf870..4d065c835 100644 --- a/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphUtils.res +++ b/src/screens/NewAnalytics/Graphs/SankyGraph/SankeyGraphUtils.res @@ -5,11 +5,75 @@ let valueFormatter = ( (this: nodeFormatter) => { let weight = this.point.options.dataLabels.name let sum = weight->Int.toFloat->NewAnalyticsUtils.valueFormatter(Volume) - let label = `${sum}
${this.point.id}` + let label = `${sum}
${this.point.id}` label } )->asTooltipPointFormatter +let tooltipFormatter = ( + @this + (this: pointFormatter) => { + let pointType = this.point.formatPrefix == "node" ? Node : Link + + let format = value => value->Int.toFloat->NewAnalyticsUtils.valueFormatter(Volume) + + let titleString = switch pointType { + | Node => this.key + | Link => `${this.point.from} -> ${this.point.to}` + } + + let info = switch pointType { + | Node => this.point.options.dataLabels.name->format + | Link => + let fromValue = this.point.fromNode.options.dataLabels.name + let toValue = this.point.toNode.options.dataLabels.name + + let (fraction, percentage) = if toValue > fromValue { + (`${fromValue->format} to ${toValue->format}`, "100%") + } else { + let percentage = toValue->Int.toFloat /. fromValue->Int.toFloat *. 100.0 + ( + `${toValue->format} of ${fromValue->format}`, + `${percentage->NewAnalyticsUtils.valueFormatter(Rate)}`, + ) + } + + `${fraction} (${percentage})` + } + + let title = `
${info}
` + + let content = ` +
+ ${title} +
+ ${titleString} +
+
` + + `
+ ${content} +
` + } +)->asTooltipPointFormatter + let getSankyGraphOptions = (payload: sankeyPayload) => { let {data, nodes, title, colors} = payload let options = { @@ -27,13 +91,14 @@ let getSankyGraphOptions = (payload: sankeyPayload) => { keys: ["from", "to", "weight", "color"], data, nodePadding: 35, - borderRadius: 0, // Set the border radius of the bars to 0 + borderRadius: 3, // Set the border radius of the bars to 0 dataLabels: { nodeFormatter: valueFormatter, style: { fontWeight: "normal", - fontSize: "13px", + fontSize: "14px", color: "#333333", + fontFamily: "Arial, sans-serif", }, allowOverlap: true, // Allow labels to overlap crop: false, // Prevent labels from being cropped @@ -44,6 +109,23 @@ let getSankyGraphOptions = (payload: sankeyPayload) => { nodes, }, ], + tooltip: { + enabled: true, + useHTML: true, + style: { + fontWeight: "normal", + fontSize: "14px", + color: "#333333", + fontFamily: "Arial, sans-serif", + }, + formatter: tooltipFormatter, + crosshairs: false, + shape: "square", + shadow: false, + backgroundColor: "transparent", + borderColor: "transparent", + borderWidth: 0.0, + }, chart: { spacingLeft: 150, spacingRight: 150, diff --git a/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycle.res b/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycle.res index cbca77d31..b3bc55ce5 100644 --- a/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycle.res +++ b/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycle.res @@ -35,22 +35,7 @@ let make = ( ] ->Dict.fromArray ->JSON.Encode.object - // Expected response - // let response = { - // "normal_success": 15, - // "normal_failure": 1, - // "cancelled": 1, - // "smart_retried_success": 1, - // "smart_retried_failure": 0, - // "pending": 0, - // "partial_refunded": 0, - // "refunded": 0, - // "disputed": 0, - // "pm_awaited": 0, - // "customer_awaited": 2, - // "merchant_awaited": 0, - // "confirmation_awaited": 0, - // }->Identity.genericTypeToJson + let paymentLifeCycleResponse = await updateDetails(url, paymentLifeCycleBody, Post) if paymentLifeCycleResponse->PaymentsLifeCycleUtils.getTotalPayments > 0 { diff --git a/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleTypes.res b/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleTypes.res index 178613928..5e225c6d4 100644 --- a/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleTypes.res +++ b/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleTypes.res @@ -8,8 +8,30 @@ type paymentLifeCycle = { partialRefunded: int, refunded: int, disputed: int, - pmAwaited: int, - customerAwaited: int, - merchantAwaited: int, - confirmationAwaited: int, + drop_offs: int, +} + +type status = + | Succeeded + | Failed + | Cancelled + | Processing + | RequiresCustomerAction + | RequiresMerchantAction + | RequiresPaymentMethod + | RequiresConfirmation + | RequiresCapture + | PartiallyCaptured + | PartiallyCapturedAndCapturable + | Full_Refunded + | Partial_Refunded + | Dispute_Present + | Null + +type query = { + count: int, + dispute_status: status, + first_attempt: int, + refunds_status: status, + status: status, } diff --git a/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleUtils.res b/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleUtils.res index 09b9a23c5..b2f744b3e 100644 --- a/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleUtils.res +++ b/src/screens/NewAnalytics/PaymentAnalytics/PaymentsLifeCycle/PaymentsLifeCycleUtils.res @@ -1,8 +1,117 @@ open SankeyGraphTypes open LogicUtils open PaymentsLifeCycleTypes + +let getstatusVariantTypeFromString = value => { + switch value { + | "succeeded" => Succeeded + | "failed" => Failed + | "cancelled" => Cancelled + | "processing" => Processing + | "requires_customer_action" => RequiresCustomerAction + | "requires_merchant_action" => RequiresMerchantAction + | "requires_payment_method" => RequiresPaymentMethod + | "requires_confirmation" => RequiresConfirmation + | "requires_capture" => RequiresCapture + | "partially_captured" => PartiallyCaptured + | "partially_captured_and_capturable" => PartiallyCapturedAndCapturable + | "full_refunded" => Full_Refunded + | "partial_refunded" => Partial_Refunded + | "dispute_present" => Dispute_Present + | _ => Null + } +} + let paymentLifeCycleResponseMapper = (json: JSON.t) => { - let valueDict = json->getDictFromJsonObject + let valueDict = + [ + "normal_success", + "normal_failure", + "pending", + "cancelled", + "drop_offs", + "smart_retried_success", + "smart_retried_failure", + "partial_refunded", + "refunded", + "disputed", + ] + ->Array.map(item => (item, 0)) + ->Dict.fromArray + + let queryItems = + json + ->getArrayFromJson([]) + ->Array.map(query => { + let queryDict = query->getDictFromJsonObject + + { + count: queryDict->getInt("count", 0), + dispute_status: queryDict->getString("dispute_status", "")->getstatusVariantTypeFromString, + first_attempt: queryDict->getInt("first_attempt", 0), + refunds_status: queryDict->getString("refunds_status", "")->getstatusVariantTypeFromString, + status: queryDict->getString("status", "")->getstatusVariantTypeFromString, + } + }) + + queryItems->Array.forEach(query => { + switch query.status { + | Succeeded => { + // normal_success or smart_retried_success + if query.first_attempt == 1 { + valueDict->Dict.set( + "normal_success", + valueDict->getInt("normal_success", 0) + query.count, + ) + } else { + valueDict->Dict.set( + "smart_retried_success", + valueDict->getInt("smart_retried_success", 0) + query.count, + ) + } + + // "refunded" or "partial_refunded" + switch query.refunds_status { + | Full_Refunded => + valueDict->Dict.set("refunded", valueDict->getInt("refunded", 0) + query.count) + | Partial_Refunded => + valueDict->Dict.set( + "partial_refunded", + valueDict->getInt("partial_refunded", 0) + query.count, + ) + | _ => () + } + + // "disputed" + switch query.dispute_status { + | Dispute_Present => + valueDict->Dict.set("disputed", valueDict->getInt("disputed", 0) + query.count) + | _ => () + } + } + | Failed => { + valueDict->Dict.set("normal_failure", valueDict->getInt("normal_failure", 0) + query.count) + if query.first_attempt != 1 { + valueDict->Dict.set( + "smart_retried_failure", + valueDict->getInt("smart_retried_failure", 0) + query.count, + ) + } + } + | Cancelled => valueDict->Dict.set("cancelled", valueDict->getInt("cancelled", 0) + query.count) + | Processing => valueDict->Dict.set("pending", valueDict->getInt("pending", 0) + query.count) + | RequiresCustomerAction + | RequiresMerchantAction + | RequiresPaymentMethod + | RequiresConfirmation + | RequiresCapture + | PartiallyCaptured + | PartiallyCapturedAndCapturable + | Null + | _ => + valueDict->Dict.set("drop_offs", valueDict->getInt("drop_offs", 0) + query.count) + } + }) { normalSuccess: valueDict->getInt("normal_success", 0), @@ -14,32 +123,17 @@ let paymentLifeCycleResponseMapper = (json: JSON.t) => { partialRefunded: valueDict->getInt("partial_refunded", 0), refunded: valueDict->getInt("refunded", 0), disputed: valueDict->getInt("disputed", 0), - pmAwaited: valueDict->getInt("pm_awaited", 0), - customerAwaited: valueDict->getInt("customer_awaited", 0), - merchantAwaited: valueDict->getInt("merchant_awaited", 0), - confirmationAwaited: valueDict->getInt("confirmation_awaited", 0), + drop_offs: valueDict->getInt("drop_offs", 0), } } let getTotalPayments = json => { let data = json->paymentLifeCycleResponseMapper - let total = - data.normalSuccess + - data.normalFailure + - data.cancelled + - data.smartRetriedSuccess + - data.smartRetriedFailure + - data.pending + - data.partialRefunded + - data.refunded + - data.disputed + - data.pmAwaited + - data.customerAwaited + - data.merchantAwaited + - data.confirmationAwaited - - total + let payment_initiated = + data.normalSuccess + data.normalFailure + data.cancelled + data.pending + data.drop_offs + + payment_initiated } let transformData = (data: array<(string, int)>) => { @@ -95,26 +189,25 @@ let paymentsLifeCycleMapper = ( let isSmartRetryEnabled = xKey->getBoolFromString(true) + let normalSuccess = data.normalSuccess + let normalFailure = data.normalFailure + let totalFailure = normalFailure + (isSmartRetryEnabled ? 0 : data.smartRetriedSuccess) + let pending = data.pending + let cancelled = data.cancelled + let dropoff = data.drop_offs let disputed = data.disputed let refunded = data.refunded let partialRefunded = data.partialRefunded - - let success = - disputed + - refunded + - partialRefunded + - (isSmartRetryEnabled ? data.smartRetriedSuccess : 0) + - data.normalSuccess - let failure = data.normalFailure + (isSmartRetryEnabled ? data.smartRetriedFailure : 0) - let pending = data.pending - let cancelled = data.cancelled - let dropoff = - data.pmAwaited + data.customerAwaited + data.merchantAwaited + data.confirmationAwaited + let smartRetriedFailure = isSmartRetryEnabled ? data.smartRetriedFailure : 0 + let smartRetriedSuccess = isSmartRetryEnabled ? data.smartRetriedSuccess : 0 + let success = normalSuccess + smartRetriedSuccess let valueDict = [ - ("Success", success), - ("Failed", failure), + ("Succeeded on First Attempt", normalSuccess), + ("Succeeded on Subsequent Attempts", smartRetriedSuccess), + ("Failed", totalFailure), + ("Smart Retried Failure", smartRetriedFailure), ("Pending", pending), ("Cancelled", cancelled), ("Drop-offs", dropoff), @@ -128,7 +221,7 @@ let paymentsLifeCycleMapper = ( }) ->transformData - let total = success + failure + pending + cancelled + dropoff + let total = success + totalFailure + pending + dropoff + cancelled let sankeyNodes = [ { @@ -140,19 +233,51 @@ let paymentsLifeCycleMapper = ( }, }, { - id: "Success", + id: "Succeeded on First Attempt", dataLabels: { align: "right", - x: -25, - name: success, + x: 183, + name: normalSuccess, }, }, { - id: "Dispute Raised", + id: "Failed", + dataLabels: { + align: "left", + x: 20, + name: totalFailure, + }, + }, + { + id: "Pending", + dataLabels: { + align: "left", + x: 20, + name: pending, + }, + }, + { + id: "Cancelled", + dataLabels: { + align: "left", + x: 20, + name: cancelled, + }, + }, + { + id: "Drop-offs", + dataLabels: { + align: "left", + x: 20, + name: dropoff, + }, + }, + { + id: "Success", dataLabels: { align: "right", - x: 105, - name: disputed, + x: -25, + name: success, }, }, { @@ -172,41 +297,35 @@ let paymentsLifeCycleMapper = ( }, }, { - id: "Pending", - dataLabels: { - align: "left", - x: 20, - name: pending, - }, - }, - { - id: "Cancelled", + id: "Dispute Raised", dataLabels: { - align: "left", - x: 20, - name: cancelled, + align: "right", + x: 105, + name: disputed, }, }, { - id: "Failed", + id: "Smart Retried Failure", dataLabels: { - align: "left", - x: 20, - name: failure, + align: "right", + x: 105, + name: smartRetriedFailure, }, }, { - id: "Drop-offs", + id: "Succeeded on Subsequent Attempts", dataLabels: { - align: "left", - x: 20, - name: dropoff, + align: "right", + x: 235, + name: smartRetriedSuccess, }, }, ] - let success = valueDict->getInt("Success", 0) - let failure = valueDict->getInt("Failed", 0) + let normalSuccess = valueDict->getInt("Succeeded on First Attempt", 0) + let smartRetriedSuccess = valueDict->getInt("Succeeded on Subsequent Attempts", 0) + let totalFailure = valueDict->getInt("Failed", 0) + let smartRetriedFailure = valueDict->getInt("Smart Retried Failure", 0) let pending = valueDict->getInt("Pending", 0) let cancelled = valueDict->getInt("Cancelled", 0) let dropoff = valueDict->getInt("Drop-offs", 0) @@ -214,30 +333,67 @@ let paymentsLifeCycleMapper = ( let refunded = valueDict->getInt("Refunds Issued", 0) let partialRefunded = valueDict->getInt("Partial Refunded", 0) - let processedData = [ - ("Payments Initiated", "Success", success, "#E4EFFF"), - ("Payments Initiated", "Failed", failure, "#F7E0E0"), - ("Payments Initiated", "Pending", pending, "#E4EFFF"), - ("Payments Initiated", "Cancelled", cancelled, "#F7E0E0"), - ("Payments Initiated", "Drop-offs", dropoff, "#F7E0E0"), - ("Success", "Dispute Raised", disputed, "#F7E0E0"), - ("Success", "Refunds Issued", refunded, "#E4EFFF"), - ("Success", "Partial Refunded", partialRefunded, "#E4EFFF"), - ] + let processedData = if isSmartRetryEnabled { + [ + ("Payments Initiated", "Succeeded on First Attempt", normalSuccess, "#E4EFFF"), + ("Payments Initiated", "Succeeded on Subsequent Attempts", smartRetriedSuccess, "#E4EFFF"), // smart retry + ("Payments Initiated", "Failed", totalFailure, "#F7E0E0"), + ("Payments Initiated", "Pending", pending, "#E4EFFF"), + ("Payments Initiated", "Cancelled", cancelled, "#F7E0E0"), + ("Payments Initiated", "Drop-offs", dropoff, "#F7E0E0"), + ("Succeeded on First Attempt", "Success", normalSuccess, "#E4EFFF"), + ("Succeeded on Subsequent Attempts", "Success", smartRetriedSuccess, "#E4EFFF"), + ("Failed", "Smart Retried Failure", smartRetriedFailure, "#F7E0E0"), // smart retry + ("Success", "Refunds Issued", refunded, "#E4EFFF"), + ("Success", "Partial Refunded", partialRefunded, "#E4EFFF"), + ("Success", "Dispute Raised", disputed, "#F7E0E0"), + ] + } else { + [ + ("Payments Initiated", "Success", normalSuccess, "#E4EFFF"), + ("Payments Initiated", "Failed", totalFailure, "#F7E0E0"), + ("Payments Initiated", "Pending", pending, "#E4EFFF"), + ("Payments Initiated", "Cancelled", cancelled, "#F7E0E0"), + ("Payments Initiated", "Drop-offs", dropoff, "#F7E0E0"), + ("Success", "Refunds Issued", refunded, "#E4EFFF"), + ("Success", "Partial Refunded", partialRefunded, "#E4EFFF"), + ("Success", "Dispute Raised", disputed, "#F7E0E0"), + ] + } let title = { text: "", } - let colors = [ - "#91B7EE", - "#91B7EE", - "#EC6262", - "#91B7EE", - "#EC6262", - "#EC6262", - "#EC6262", - "#91B7EE", - ] + + let colors = if isSmartRetryEnabled { + [ + "#91B7EE", + "#91B7EE", + "#91B7EE", + "#EC6262", + "#91B7EE", + "#EC6262", + "#EC6262", + "#91B7EE", + "#EC6262", + "#91B7EE", + "#91B7EE", + "#EC6262", + ] + } else { + [ + "#91B7EE", + "#91B7EE", + "#EC6262", + "#91B7EE", + "#EC6262", + "#EC6262", + "#91B7EE", + "#91B7EE", + "#EC6262", + "#EC6262", + ] + } {data: processedData, nodes: sankeyNodes, title, colors} }