summaryrefslogtreecommitdiff
path: root/backend/api/src/stripe.zig
diff options
context:
space:
mode:
Diffstat (limited to 'backend/api/src/stripe.zig')
-rw-r--r--backend/api/src/stripe.zig181
1 files changed, 181 insertions, 0 deletions
diff --git a/backend/api/src/stripe.zig b/backend/api/src/stripe.zig
new file mode 100644
index 0000000..87cef10
--- /dev/null
+++ b/backend/api/src/stripe.zig
@@ -0,0 +1,181 @@
+const z = @import( "std" );
+const db = @import( "db.zig" );
+const req = @import( "req.zig" );
+const net = @import( "net-util.zig" );
+const zap = @import( "zap" );
+const jwt = @import( "jwt" );
+const mail = @import( "mail.zig" );
+const config = @import( "config.zig" );
+
+const u = struct {
+ usingnamespace @import( "util.zig" );
+ usingnamespace @import( "userdefs.zig" );
+ usingnamespace @import( "user.zig" );
+};
+
+const ArenaAllocator = z.heap.ArenaAllocator;
+const ErrorRes = net.ErrorResponse;
+const Status = zap.StatusCode;
+const ArrayList = z.ArrayList;
+const OkRes = net.OkResponse;
+const Request = zap.Request;
+const JWT = jwt.JWT;
+
+const alloc = u.alloc;
+const memeql = z.mem.eql;
+
+pub const routes = .{
+ .@"create-payment-intent" = createPaymentIntent
+};
+
+pub const SubLength = enum(u32) {
+ week = 1,
+ month = 2,
+ year = 3,
+
+ pub fn getTime( self: SubLength ) i64 {
+ switch( self ) {
+ .week => return 7 * 24 * 60 * 60 * 1000,
+ .month => return 30 * 24 * 60 * 60 * 1000,
+ .year => return 365 * 24 * 60 * 60 * 1000
+ }
+ }
+
+ pub fn getCostAmount( self: SubLength ) u32 {
+ switch( self ) {
+ .week => return 270,
+ .month => return 1000,
+ .year => return 10000
+ }
+ }
+};
+
+const StripeUrlParams = struct {
+ amount: u32 = 1000,
+ currency: []const u8 = "usd",
+ description: []const u8 = "Axonbox premium subscription",
+ payment_method: []const u8,
+ confirm: []const u8 = "true",
+ uuid: []const u8,
+ return_url: []const u8
+};
+
+const StripeRequestParams = struct {
+ paymentMethodId: []const u8,
+ subLength: SubLength = .month,
+
+ pub const @"getty.db" = u.@"json.ignore.unknown";
+};
+
+const StripeResponse = struct {
+ status: []const u8,
+
+ pub const @"getty.db" = u.@"json.ignore.unknown";
+};
+
+///freed by caller
+fn formatStripeParams( params: StripeUrlParams ) []const u8 {
+ const ret = z.fmt.allocPrint( alloc,
+ "amount={d}&currency={s}&description={s}&payment_method={s}&confirm={s}&metadata%5Buuid%5D={s}&return_url={s}",
+ .{
+ params.amount,
+ params.currency,
+ params.description,
+ params.payment_method,
+ params.confirm,
+ params.uuid,
+ params.return_url
+ }
+ ) catch return "";
+
+ return ret;
+}
+
+fn formatReturnUrl( buf: []u8 ) []const u8 {
+ return z.fmt.bufPrintZ( buf, "{s}/payment-success.html", .{
+ config.server_url
+ } ) catch return "";
+}
+
+fn getStripeKeyBearer() ![]const u8 {
+ const file = try u.readFile( "../data/stripe_key.txt" );
+ defer alloc.free( file );
+
+ const buf = try z.fmt.allocPrint( alloc, "Bearer {s}", .{ file } );
+ return buf;
+}
+
+pub fn handleStripeResponse( user: *u.UserEntry, res: StripeResponse, r: zap.Request, length: SubLength ) void {
+ const status = res.status;
+ if( memeql( u8, status, "succeeded" ) ) {
+ user.subscription_data = u.SubData{
+ .plan = "paid",
+ .endTime = z.time.milliTimestamp() + length.getTime(),
+ .reminded = false
+ };
+
+ u.updateEntry( user.* ) catch {
+ return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "failed to update user" } );
+ };
+ return net.sendJson( r, .ok, OkRes( .{ .msg = "success", .paymentIntent = res } ) );
+ }
+
+ net.sendJson( r, .bad_request, ErrorRes{ .msg = "failed" } );
+}
+
+
+///route @/create-payment-intent
+pub fn createPaymentIntent( r: zap.Request ) void {
+ if( net.handleInvalidPostReq( r ) ) return;
+ u.checkDbOnThread() catch return;
+
+ var token = u.tokenFromPostReq( r ) catch return;
+ defer token.deinit();
+
+ const params = u.jsonParse( StripeRequestParams, r.body.? ) catch {
+ return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid request format" } );
+ }; defer params.deinit();
+
+ var user = u.getEntry( token.claims.uuid ) catch {
+ return net.sendJson( r, .unauthorized, ErrorRes{ .msg = "user not found" } );
+ }; defer user.db_entry.?.deinit();
+
+ if( !memeql( u8, user.subscription_data.plan, "free" ) ) {
+ return net.sendJson( r, .bad_request, ErrorRes{ .msg = "user already subscribed" } );
+ }
+
+ const stripe_key = getStripeKeyBearer() catch {
+ return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } );
+ }; defer alloc.free( stripe_key );
+
+ const headers = [_]z.http.Header{
+ .{ .name = "Authorization", .value = stripe_key }
+ };
+
+ var buf: [2048]u8 = undefined;
+ const body = formatStripeParams( .{
+ .amount = params.v.subLength.getCostAmount(),
+ .payment_method = params.v.paymentMethodId,
+ .uuid = user.uuid,
+ .return_url = formatReturnUrl( &buf )
+ } ); defer alloc.free( body );
+
+ var res = req.send( .{
+ .url = "https://api.stripe.com/v1/payment_intents",
+ .method = .POST,
+ .headers = &headers,
+ .body = body
+ }, alloc );
+ defer res.deinit();
+
+ if( !res.ok ) {
+ z.debug.print( "failed to create payment intent: {s}\n", .{ res.body.? } );
+ return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "failed to create payment intent" } );
+ }
+
+ const stripe_json = u.jsonParse( StripeResponse, res.body.? ) catch {
+ return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "failed to parse provider response" } );
+ }; defer stripe_json.deinit();
+
+ handleStripeResponse( &user, stripe_json.v, r, params.v.subLength );
+}