1
+ from typing import Union
2
+
3
+ from posthog .hogql import ast
4
+ from posthog .schema import CurrencyCode , RevenueTrackingConfig , RevenueTrackingEventItem
1
5
from posthog .hogql .database .models import (
2
6
StringDatabaseField ,
3
7
DateDatabaseField ,
@@ -19,3 +23,154 @@ def to_printed_clickhouse(self, context):
19
23
20
24
def to_printed_hogql (self ):
21
25
return "exchange_rate"
26
+
27
+
28
+ def convert_currency_call (
29
+ amount : ast .Expr , currency_from : ast .Expr , currency_to : ast .Expr , timestamp : ast .Expr | None = None
30
+ ) -> ast .Expr :
31
+ args = [currency_from , currency_to , amount ]
32
+ if timestamp :
33
+ args .append (timestamp )
34
+
35
+ return ast .Call (name = "convertCurrency" , args = args )
36
+
37
+
38
+ def revenue_currency_expression (config : RevenueTrackingConfig ) -> ast .Expr :
39
+ exprs = []
40
+ for event in config .events :
41
+ exprs .extend (
42
+ [
43
+ ast .CompareOperation (
44
+ left = ast .Field (chain = ["event" ]),
45
+ op = ast .CompareOperationOp .Eq ,
46
+ right = ast .Constant (value = event .eventName ),
47
+ ),
48
+ ast .Field (chain = ["events" , "properties" , event .revenueCurrencyProperty ])
49
+ if event .revenueCurrencyProperty
50
+ else ast .Constant (value = None ),
51
+ ]
52
+ )
53
+
54
+ if len (exprs ) == 0 :
55
+ return ast .Constant (value = None )
56
+
57
+ # Else clause, make sure there's a None at the end
58
+ exprs .append (ast .Constant (value = None ))
59
+
60
+ return ast .Call (name = "multiIf" , args = exprs )
61
+
62
+
63
+ def revenue_comparison_and_value_exprs (
64
+ event : RevenueTrackingEventItem ,
65
+ config : RevenueTrackingConfig ,
66
+ do_currency_conversion : bool = False ,
67
+ ) -> tuple [ast .Expr , ast .Expr ]:
68
+ # Check whether the event is the one we're looking for
69
+ comparison_expr = ast .CompareOperation (
70
+ left = ast .Field (chain = ["event" ]),
71
+ op = ast .CompareOperationOp .Eq ,
72
+ right = ast .Constant (value = event .eventName ),
73
+ )
74
+
75
+ # If there's a revenueCurrencyProperty, convert the revenue to the base currency from that property
76
+ # Otherwise, assume we're already in the base currency
77
+ # Also, assume that `base_currency` is USD by default, it'll be empty for most customers
78
+ if event .revenueCurrencyProperty and do_currency_conversion :
79
+ value_expr = ast .Call (
80
+ name = "if" ,
81
+ args = [
82
+ ast .Call (
83
+ name = "isNull" , args = [ast .Field (chain = ["events" , "properties" , event .revenueCurrencyProperty ])]
84
+ ),
85
+ ast .Call (
86
+ name = "toDecimal" ,
87
+ args = [
88
+ ast .Field (chain = ["events" , "properties" , event .revenueProperty ]),
89
+ ast .Constant (value = 10 ),
90
+ ],
91
+ ),
92
+ convert_currency_call (
93
+ ast .Field (chain = ["events" , "properties" , event .revenueProperty ]),
94
+ ast .Field (chain = ["events" , "properties" , event .revenueCurrencyProperty ]),
95
+ ast .Constant (value = (config .baseCurrency or CurrencyCode .USD ).value ),
96
+ ast .Call (name = "DATE" , args = [ast .Field (chain = ["events" , "timestamp" ])]),
97
+ ),
98
+ ],
99
+ )
100
+ else :
101
+ value_expr = ast .Call (
102
+ name = "toDecimal" ,
103
+ args = [ast .Field (chain = ["events" , "properties" , event .revenueProperty ]), ast .Constant (value = 10 )],
104
+ )
105
+
106
+ return (comparison_expr , value_expr )
107
+
108
+
109
+ def revenue_expression (
110
+ config : Union [RevenueTrackingConfig , dict , None ],
111
+ do_currency_conversion : bool = False ,
112
+ ) -> ast .Expr :
113
+ if isinstance (config , dict ):
114
+ config = RevenueTrackingConfig .model_validate (config )
115
+
116
+ if not config or not config .events :
117
+ return ast .Constant (value = None )
118
+
119
+ exprs : list [ast .Expr ] = []
120
+ for event in config .events :
121
+ comparison_expr , value_expr = revenue_comparison_and_value_exprs (event , config , do_currency_conversion )
122
+ exprs .extend ([comparison_expr , value_expr ])
123
+
124
+ # Else clause, make sure there's a None at the end
125
+ exprs .append (ast .Constant (value = None ))
126
+
127
+ return ast .Call (name = "multiIf" , args = exprs )
128
+
129
+
130
+ def revenue_sum_expression (
131
+ config : Union [RevenueTrackingConfig , dict , None ],
132
+ do_currency_conversion : bool = False ,
133
+ ) -> ast .Expr :
134
+ if isinstance (config , dict ):
135
+ config = RevenueTrackingConfig .model_validate (config )
136
+
137
+ if not config or not config .events :
138
+ return ast .Constant (value = None )
139
+
140
+ exprs : list [ast .Expr ] = []
141
+ for event in config .events :
142
+ comparison_expr , value_expr = revenue_comparison_and_value_exprs (event , config , do_currency_conversion )
143
+
144
+ exprs .append (
145
+ ast .Call (
146
+ name = "sumIf" ,
147
+ args = [
148
+ ast .Call (name = "ifNull" , args = [value_expr , ast .Constant (value = 0 )]),
149
+ comparison_expr ,
150
+ ],
151
+ )
152
+ )
153
+
154
+ if len (exprs ) == 1 :
155
+ return exprs [0 ]
156
+
157
+ return ast .Call (name = "plus" , args = exprs )
158
+
159
+
160
+ def revenue_events_where_expr (config : Union [RevenueTrackingConfig , dict , None ]) -> ast .Expr :
161
+ if isinstance (config , dict ):
162
+ config = RevenueTrackingConfig .model_validate (config )
163
+
164
+ if not config or not config .events :
165
+ return ast .Constant (value = False )
166
+
167
+ exprs : list [ast .Expr ] = []
168
+ for event in config .events :
169
+ # NOTE: Dont care about conversion, only care about comparison which is independent of conversion
170
+ comparison_expr , _value_expr = revenue_comparison_and_value_exprs (event , config , do_currency_conversion = False )
171
+ exprs .append (comparison_expr )
172
+
173
+ if len (exprs ) == 1 :
174
+ return exprs [0 ]
175
+
176
+ return ast .Or (exprs = exprs )
0 commit comments