Skip to main content

gatekpr_patterns/
billing.rs

1//! Billing-related patterns for Shopify Billing API compliance
2//!
3//! Includes patterns for Shopify Billing API usage and third-party payment detection.
4
5use crate::registry::PatternRegistry;
6use once_cell::sync::Lazy;
7
8/// Pre-built billing pattern registry
9pub static BILLING_PATTERNS: Lazy<PatternRegistry> = Lazy::new(|| {
10    let mut registry = PatternRegistry::new();
11
12    // Shopify Billing API patterns
13    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    // Third-party payment patterns (not allowed for public apps)
49    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
94/// Pattern keys for Shopify Billing API
95pub const SHOPIFY_BILLING_KEYS: &[&str] = &[
96    "app_subscription_create",
97    "recurring_charge",
98    "usage_charge",
99    "one_time_charge",
100    "billing_api",
101];
102
103/// Pattern keys for third-party payment providers
104pub 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
113/// Check billing compliance in code
114pub 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/// Billing compliance status
134#[derive(Debug, Clone, Default)]
135pub struct BillingStatus {
136    /// Whether Shopify Billing API is used
137    pub uses_shopify_billing: bool,
138    /// List of third-party payment providers detected
139    pub third_party_detected: Vec<String>,
140    /// Whether subscription billing is implemented
141    pub has_subscription: bool,
142    /// Whether usage-based billing is implemented
143    pub has_usage_billing: bool,
144    /// Whether one-time charges are implemented
145    pub has_one_time: bool,
146}
147
148impl BillingStatus {
149    /// Check if billing is compliant (uses Shopify, no third-party)
150    pub fn is_compliant(&self) -> bool {
151        self.uses_shopify_billing && self.third_party_detected.is_empty()
152    }
153
154    /// Get compliance status string
155    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    /// Check if any billing is implemented
166    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}