gatekpr_patterns/
billing.rs1use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8pub static BILLING_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10 let mut registry = PatternRegistry::new();
11
12 registry
14 .register(
15 "app_subscription_create",
16 r"(?i)(appSubscriptionCreate|app_subscription_create|createAppSubscription)",
17 )
18 .unwrap();
19
20 registry
21 .register(
22 "recurring_charge",
23 r"(?i)(recurring_application_charge|RecurringApplicationCharge|recurringCharge)",
24 )
25 .unwrap();
26
27 registry
28 .register(
29 "usage_charge",
30 r"(?i)(usage_charge|UsageCharge|appUsageRecord|usageRecord)",
31 )
32 .unwrap();
33
34 registry
35 .register(
36 "one_time_charge",
37 r"(?i)(application_charge|ApplicationCharge|oneTimeCharge|appPurchaseOneTime)",
38 )
39 .unwrap();
40
41 registry
42 .register(
43 "billing_api",
44 r"(?i)(shopify.*billing|billing.*shopify|/admin/api/.*/(recurring_application_charges|application_charges))",
45 )
46 .unwrap();
47
48 registry
50 .register(
51 "stripe_integration",
52 r"(?i)(stripe\.|@stripe/|stripe-js|new\s+Stripe\s*\(|StripeClient|createPaymentIntent|PaymentElement)",
53 )
54 .unwrap();
55
56 registry
57 .register(
58 "paypal_integration",
59 r"(?i)(paypal\.|@paypal/|paypal-js|PayPalButtons|paypalSdk)",
60 )
61 .unwrap();
62
63 registry
64 .register(
65 "square_integration",
66 r"(?i)(square\.|squareup|SquareClient|square-web-sdk)",
67 )
68 .unwrap();
69
70 registry
71 .register(
72 "braintree_integration",
73 r"(?i)(braintree\.|braintree-web|BraintreeClient|braintreeGateway)",
74 )
75 .unwrap();
76
77 registry
78 .register(
79 "adyen_integration",
80 r"(?i)(adyen\.|@adyen/|AdyenCheckout|adyenPayment)",
81 )
82 .unwrap();
83
84 registry
85 .register(
86 "generic_payment_gateway",
87 r"(?i)(payment.*gateway|checkout.*session|process.*payment|charge.*card)",
88 )
89 .unwrap();
90
91 registry
92});
93
94pub const SHOPIFY_BILLING_KEYS: &[&str] = &[
96 "app_subscription_create",
97 "recurring_charge",
98 "usage_charge",
99 "one_time_charge",
100 "billing_api",
101];
102
103pub const THIRD_PARTY_PAYMENT_KEYS: &[&str] = &[
105 "stripe_integration",
106 "paypal_integration",
107 "square_integration",
108 "braintree_integration",
109 "adyen_integration",
110 "generic_payment_gateway",
111];
112
113pub fn check_billing_compliance(text: &str) -> BillingStatus {
115 let mut third_party_detected = Vec::new();
116
117 for key in THIRD_PARTY_PAYMENT_KEYS {
118 if BILLING_PATTERNS.is_match(key, text) {
119 third_party_detected.push(key.to_string());
120 }
121 }
122
123 BillingStatus {
124 uses_shopify_billing: BILLING_PATTERNS.any_match(SHOPIFY_BILLING_KEYS, text),
125 third_party_detected,
126 has_subscription: BILLING_PATTERNS.is_match("app_subscription_create", text)
127 || BILLING_PATTERNS.is_match("recurring_charge", text),
128 has_usage_billing: BILLING_PATTERNS.is_match("usage_charge", text),
129 has_one_time: BILLING_PATTERNS.is_match("one_time_charge", text),
130 }
131}
132
133#[derive(Debug, Clone, Default)]
135pub struct BillingStatus {
136 pub uses_shopify_billing: bool,
138 pub third_party_detected: Vec<String>,
140 pub has_subscription: bool,
142 pub has_usage_billing: bool,
144 pub has_one_time: bool,
146}
147
148impl BillingStatus {
149 pub fn is_compliant(&self) -> bool {
151 self.uses_shopify_billing && self.third_party_detected.is_empty()
152 }
153
154 pub fn status(&self) -> &'static str {
156 if !self.third_party_detected.is_empty() {
157 "fail"
158 } else if self.uses_shopify_billing {
159 "pass"
160 } else {
161 "warning"
162 }
163 }
164
165 pub fn has_billing(&self) -> bool {
167 self.uses_shopify_billing || !self.third_party_detected.is_empty()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_shopify_billing_detection() {
177 let code = r#"
178 const response = await client.mutate({
179 mutation: appSubscriptionCreate,
180 variables: { name: "Pro Plan", lineItems: [...] }
181 });
182 "#;
183
184 let status = check_billing_compliance(code);
185 assert!(status.uses_shopify_billing);
186 assert!(status.has_subscription);
187 assert!(status.third_party_detected.is_empty());
188 assert!(status.is_compliant());
189 }
190
191 #[test]
192 fn test_stripe_detection() {
193 let code = r#"
194 import Stripe from 'stripe';
195 const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
196 const paymentIntent = await stripe.paymentIntents.create({...});
197 "#;
198
199 let status = check_billing_compliance(code);
200 assert!(!status.third_party_detected.is_empty());
201 assert!(status
202 .third_party_detected
203 .contains(&"stripe_integration".to_string()));
204 assert!(!status.is_compliant());
205 }
206
207 #[test]
208 fn test_paypal_detection() {
209 let code = r#"
210 import { PayPalButtons } from "@paypal/react-paypal-js";
211 <PayPalButtons createOrder={...} />
212 "#;
213
214 let status = check_billing_compliance(code);
215 assert!(status
216 .third_party_detected
217 .contains(&"paypal_integration".to_string()));
218 assert!(!status.is_compliant());
219 }
220
221 #[test]
222 fn test_usage_billing() {
223 let code = r#"
224 const usageRecord = await client.mutate({
225 mutation: appUsageRecordCreate,
226 variables: { subscriptionLineItemId, price: "0.05" }
227 });
228 "#;
229
230 let status = check_billing_compliance(code);
231 assert!(status.has_usage_billing);
232 }
233
234 #[test]
235 fn test_compliant_code() {
236 let code = r#"
237 // Using Shopify Billing API for subscriptions
238 const subscription = await admin.graphql(`
239 mutation appSubscriptionCreate($name: String!, $lineItems: [AppSubscriptionLineItemInput!]!) {
240 appSubscriptionCreate(name: $name, lineItems: $lineItems) {
241 appSubscription { id }
242 }
243 }
244 `);
245 "#;
246
247 let status = check_billing_compliance(code);
248 assert!(status.is_compliant());
249 assert_eq!(status.status(), "pass");
250 }
251
252 #[test]
253 fn test_no_billing() {
254 let code = r#"
255 // Simple app with no billing
256 function getProducts() {
257 return fetch('/api/products');
258 }
259 "#;
260
261 let status = check_billing_compliance(code);
262 assert!(!status.has_billing());
263 assert_eq!(status.status(), "warning");
264 }
265}