import assert from "assert"; import { RawEvent, NormalizedEvent } from "../../types"; const emptyRecord = { id: null, ts: null, text: null, href: null, is_bad: null, direction: null, source: null, stream: null }; const emptyUser = { source_user_id: null, user_id: null, name: null, email: null, ip: null, avatar: null, nickname: null, phone: null, city: null, continent_code: null, country: null, postal_code: null, region: null, timezone: null, country_code: null, address_line1: null, address_line2: null, user_agent: null, is_employee: null, title: null, birthday: null, created_at: null, // We don't actually know when the *user* was created since the created is on the event, not the user here age: null, gender: null, website: null, custom_attributes: null }; const emptyCompany = { name: null, id: null, industry: null, employee_count: null, plan: null }; const fillWithNulls = record => { return { ...emptyRecord, ...record, user_info: { ...emptyUser, ...(record.user_info || {}), company: { ...emptyCompany, ...((record.user_info && record.user_info.company) || {}) } } }; }; const getText = (event: RawEvent) => { return ( ({}[event.type] || event.type .split(".") .map(str => str .split("_") .map(str => str.charAt(0).toUpperCase() + str.slice(1)) .join(" ") ) .join(" ")) + (event.data.object.description ? ` : ${event.data.object.description}` : "") ); }; const noPII = (event: RawEvent) => { return { ...emptyRecord, id: `stripe|${event.id}`, ts: Date.parse(event.created), text: getText(event), source: "stripe", stream: "events", href: `https://dashboard.stripe.com/events/${event.id}`, user_info: { ...emptyUser, company: { ...emptyCompany } } }; }; const withCusId = (event: RawEvent) => { const rtn = noPII(event); rtn.user_info.source_user_id = event.data.object.customer; return rtn; }; const withCardholder = (event: RawEvent) => { const rtn = noPII(event); rtn.user_info.source_user_id = event.data.object.customer; return rtn; }; const parseBillingDetails = billingDetails => { if (!billingDetails) return {}; return { ...parseAddress(billingDetails.address), email: billingDetails.email, name: billingDetails.name, phone: billingDetails.phone }; }; const parseAddress = address => { if (!address) return {}; return { city: address.city, country_code: address.country, address_line1: address.line1, address_line2: address.line2, postal_code: address.postal_code, region: address.state }; }; const parseShipping = shipping => { if (!shipping) return {}; return { ...parseAddress(shipping.address), name: shipping.name, phone: shipping.phone }; }; const typeToInfo = (event: RawEvent) => { switch (event.data.object.object) { case "account": return { id: `stripe|${event.id}`, ts: Date.parse(event.created), user_info: { source_user_id: (event.data.object.individual && event.data.object.individual.id) || null, user_id: null, name: (event[event.business_type] && event[event.business_type].name) || null, email: event.email, ip: null, avatar: null, nickname: null, phone: event[event.business_type].phone, city: event[event.business_type].address.city, continent_code: null, country: null, postal_code: event.data.object[event.business_type].address.postal_code, region: null, timezone: null, country_code: event.data.object.country || event[event.business_type].address.country, address_line1: null, address_line2: null, user_agent: null, is_employee: null, title: null, company: { name: null, id: null, industry: null, employee_count: null, plan: null }, created_at: null, website: null, birthday: (event.data.object.individual && event.data.object.individual.dob && new Date( event.data.object.individual.dob.year, event.data.object.individual.month - 1, event.data.object.individual.day )) || null, age: null, gender: (event.data.object.individual && event.data.object.individual.gender) || null, custom_attributes: null }, text: getText(event), href: `https://dashboard.stripe.com/accounts/${event.data.object.id}`, is_bad: null, direction: null, source: "stripe", stream: "events" }; case "application_fee": return noPII(event); case "balance": return { ...noPII(event), href: `https://dashboard.stripe.com/balance` }; case "capability": return noPII(event); case "charge": return { ...noPII(event), user_info: { ...parseBillingDetails(event.data.object.billing_details), source_user_id: event.data.object.customer }, is_bad: event.data.object.status === "failed", href: `https://dashboard.stripe.com/payments/${event.data.object.id}`, direction: { failed: "inbound", refunded: "outbound", pending: "outbound", succeeded: "inbound", "dispute.created": "inbound" }[event.type.slice("charge.".length)] || null }; // checkout //TODO, case "coupon": return { ...noPII(event), href: `https://dashboard.stripe.com/coupons/${event.data.object.id}` }; case "credit_note": return withCusId(event); case "customer": return { id: `stripe|${event.id}`, ts: Date.parse(event.created), user_info: { ...parseShipping(event.data.object.shipping), source_user_id: event.data.object.id, user_id: null, name: event.data.object.name, email: event.data.object.email, ip: null, avatar: null, nickname: null, phone: event.data.object.phone, continent_code: null, country: null, region: null, timezone: null, address_line1: null, address_line2: null, user_agent: null, is_employee: null, title: null, company: { name: null, id: null, industry: null, employee_count: null, plan: null }, created_at: null, website: null, birthday: null, age: null, gender: null, custom_attributes: null }, text: getText(event), href: `https://dashboard.stripe.com/customer/${event.data.object.id}`, is_bad: null, direction: { "coupon.created": "inbound", "source.created": "inbound", "subscription.created": "inbound" }[event.type.slice("customer.".length)] || null, source: "stripe", stream: "events" }; case "file": return noPII(event); case "invoice": return { ...noPII(event), user_info: { ...parseAddress(event.data.object.customer_address), source_user_id: event.data.object.customer, name: event.data.object.customer_name, email: event.data.object.customer_email, phone: event.data.object.customer_phone }, direction: { finalized: "outbound", payment_failed: "inbound", payment_succeeded: "inbound", sent: "outbound", voided: "outbound" }[event.type.slice("invoice.".length)] || null }; case "invoiceitem": return withCusId(event); case "issuing.authorization": return withCardholder(event); case "issuing.dispute": return noPII(event); case "order": return { ...noPII(event), user_info: { ...parseShipping(event.data.object.shipping), email: event.data.object.email, source_user_id: event.data.object.customer }, direction: { created: "inbound", payment_failed: "inbound", payment_succeeded: "inbound" }[event.type.slice("order.".length)] || null }; case "order_return": return { ...noPII(event), direction: event.type === "order_return.created" ? "inbound" : null }; case "payment_intent": return { ...noPII(event), href: `https://dashboard.stripe.com/payments/${event.data.object.id}`, user_info: { ...parseShipping(event.data.object.shipping), email: event.data.object.recipt_email, source_user_id: event.data.object.customer }, direction: { created: "inbound", canceled: "inbound", payment_failed: "inbound", succeeded: "inbound" }[event.type.slice("payment_intent.".length)] || null }; case "payment_method": return { ...noPII(event), user_info: { ...parseBillingDetails(event.data.object.billing_details), source_user_id: event.data.object.customer }, direction: { attached: "inbound", detached: "inbound" }[event.type.slice("payment_method.".length)] || null }; case "payout": return { ...noPII(event), href: `https://dashboard.stripe.com/payouts/${event.data.object.id}`, is_bad: Boolean(event.data.object.failure_code) }; case "plan": return noPII(event); case "product": return noPII(event); case "radar.early_fraud_warning": return noPII(event); case "setup_intent": return withCusId(event); case "sku": return noPII(event); case "tax_rate": return noPII(event); case "transfer": return noPII(event); default: return noPII(event); } }; export default (event: RawEvent): NormalizedEvent<"stripe"> => { assert(event, `Stripe:Events null event: ${event}`); assert( event.id, `Stripe:Events event is missing the required field id ${event}` ); assert( event.created, `Stripe:Events event is missing the required field created ${event}` ); const obj = typeToInfo(event); return fillWithNulls(obj); };