add orders collecrtion for coins and subscriptions page

This commit is contained in:
Thomas Demirdjian
2025-11-05 11:18:43 +01:00
parent ffaccd71a7
commit a7371323c4
17 changed files with 1613 additions and 228 deletions
+1
View File
@@ -50,3 +50,4 @@ exports.rankings = require("./src/rankings");
exports.payouts = require("./src/payouts");
exports.subscription = require("./src/subscription");
exports.youtube = require("./src/youtube");
exports.orders = require("./src/orders");
+416 -12
View File
@@ -68,6 +68,16 @@
"node": ">=6.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
"integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -137,6 +147,34 @@
"integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==",
"license": "MIT"
},
"node_modules/@ffmpeg-installer/darwin-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
"cpu": [
"arm64"
],
"hasInstallScript": true,
"license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@ffmpeg-installer/darwin-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
"cpu": [
"x64"
],
"hasInstallScript": true,
"license": "LGPL-2.1",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@ffmpeg-installer/ffmpeg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
@@ -153,6 +191,48 @@
"@ffmpeg-installer/win32-x64": "4.1.0"
}
},
"node_modules/@ffmpeg-installer/linux-arm": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
"cpu": [
"arm"
],
"hasInstallScript": true,
"license": "GPLv3",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
"cpu": [
"arm64"
],
"hasInstallScript": true,
"license": "GPLv3",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
"cpu": [
"ia32"
],
"hasInstallScript": true,
"license": "GPLv3",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
@@ -167,6 +247,32 @@
"linux"
]
},
"node_modules/@ffmpeg-installer/win32-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
"cpu": [
"ia32"
],
"license": "GPLv3",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@ffmpeg-installer/win32-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
"cpu": [
"x64"
],
"license": "GPLv3",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@firebase/app-check-interop-types": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz",
@@ -433,6 +539,8 @@
"version": "7.11.6",
"resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz",
"integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api": "^1.3.0",
"fast-deep-equal": "^3.1.1",
@@ -622,9 +730,9 @@
}
},
"node_modules/@google-cloud/storage": {
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.2.tgz",
"integrity": "sha512-6xN0KNO8L/LIA5zu3CJwHkJiB6n65eykBLOb0E+RooiHYgX8CSao6lvQiKT9TBk2gL5g33LL3fmhDodZnt56rw==",
"version": "7.17.3",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.3.tgz",
"integrity": "sha512-gOnCAbFgAYKRozywLsxagdevTF7Gm+2Ncz5u5CQAuOv/2VCa0rdGJWvJFDOftPx1tc+q8TXiC2pEJfFKu+yeMQ==",
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/paginator": "^5.0.0",
@@ -743,6 +851,130 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
@@ -759,6 +991,22 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
@@ -775,6 +1023,72 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
@@ -797,6 +1111,28 @@
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
@@ -819,6 +1155,63 @@
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
@@ -872,6 +1265,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -967,6 +1361,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
"integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "1.25.1"
},
@@ -2154,6 +2549,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
"integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1"
@@ -2196,6 +2592,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
"integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1",
@@ -2249,6 +2646,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
"integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1",
@@ -2759,6 +3157,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2937,9 +3336,9 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -3586,6 +3985,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4080,6 +4480,7 @@
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz",
"integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@fastify/busboy": "^3.0.0",
"@firebase/database-compat": "1.0.8",
@@ -4367,6 +4768,7 @@
"resolved": "https://registry.npmjs.org/genkit/-/genkit-1.22.0.tgz",
"integrity": "sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@genkit-ai/ai": "1.22.0",
"@genkit-ai/core": "1.22.0",
@@ -6047,9 +6449,9 @@
"license": "MIT"
},
"node_modules/resend": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.4.0.tgz",
"integrity": "sha512-CTr4ix4RI5M/ucL58Wqr+LE8eI4JHtJEFaBAx6yUVNOI3eaPVtJjpNL0G/BdRSWMbwv6CtpprVOY8Xvpp6UJlA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/resend/-/resend-6.4.1.tgz",
"integrity": "sha512-+P1gF6bYT0lKd0vsYyKKTHd6PfMH37yDgNRA6wAARQgQbYlc2BZYuo9sB/uZKkVg1oGSh0cgpnxNKGsZJutXMg==",
"license": "MIT",
"dependencies": {
"svix": "1.76.1"
@@ -6546,9 +6948,9 @@
}
},
"node_modules/stripe": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.0.tgz",
"integrity": "sha512-strzN8luMGMC1LEleGKg7pJGXFx0kSS4y/uSjK8yPQV9SUBMtJVAp/v8XMQLRnMbXaSaWLrIaHcMlKcsizdRDQ==",
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz",
"integrity": "sha512-eRc2T413316D7fAjSSEgPbJJ+4a8KY9rOTOb27aXd7bkw9ADO/3OxmIk7YWDhWvHgvxnEZ/29YjcmBBOu4mhrw==",
"license": "MIT",
"dependencies": {
"qs": "^6.11.0"
@@ -6962,6 +7364,7 @@
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8",
@@ -7126,6 +7529,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+90
View File
@@ -0,0 +1,90 @@
const admin = require("firebase-admin");
const ORDER_TYPES = {
GIFT: "GIFT",
SONG: "SONG",
COINS: "COINS",
};
const ORDER_STATUS = {
PENDING: "PENDING",
APPLIED: "APPLIED",
REJECTED: "REJECTED",
};
const ORDERS_COLLECTION = "orders";
const isFiniteNumber = (value) => {
if (typeof value === "number" && Number.isFinite(value)) {
return true;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isFinite(parsed);
}
return false;
};
const normalizeAmount = (amount) => {
if (!isFiniteNumber(amount)) {
return null;
}
return Number(amount);
};
const createOrderDocument = async ({
userId,
type,
amount,
songId = null,
createdBy = "system",
metadata = {},
orderId = null,
}) => {
if (!userId || typeof userId !== "string") {
throw new Error("[orders] Missing userId when creating order");
}
if (!Object.values(ORDER_TYPES).includes(type)) {
throw new Error(`[orders] Invalid order type "${type}"`);
}
const normalizedAmount = normalizeAmount(amount);
if (normalizedAmount === null || normalizedAmount === 0) {
throw new Error("[orders] Invalid order amount");
}
const payload = {
userId,
type,
amount: normalizedAmount,
songId: type === ORDER_TYPES.SONG ? songId || null : null,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
createdBy: createdBy || "system",
status: ORDER_STATUS.PENDING,
metadata: metadata || {},
};
const collectionRef = admin.firestore().collection(ORDERS_COLLECTION);
const orderRef = orderId ? collectionRef.doc(orderId) : collectionRef.doc();
if (orderId) {
const existingSnapshot = await orderRef.get();
if (existingSnapshot.exists) {
return { orderRef, orderId: orderRef.id };
}
}
await orderRef.set(payload);
return { orderRef, orderId: orderRef.id };
};
module.exports = {
ORDER_TYPES,
ORDER_STATUS,
ORDERS_COLLECTION,
createOrderDocument,
normalizeAmount,
};
+190
View File
@@ -0,0 +1,190 @@
const admin = require("firebase-admin");
const { onDocumentCreated } = require("firebase-functions/firestore");
const { HttpsError, onCall } = require("firebase-functions/https");
const { REGION } = require("../index");
const {
ORDER_TYPES,
ORDER_STATUS,
ORDERS_COLLECTION,
createOrderDocument,
normalizeAmount,
} = require("./helpers/orders");
const USERS_COLLECTION = "users";
const onOrderCreated = onDocumentCreated(
`${ORDERS_COLLECTION}/{orderId}`,
async (event) => {
const orderRef = event?.data?.ref;
const orderData = event?.data?.data();
if (!orderRef || !orderData) {
return;
}
if (orderData?.processedAt) {
return;
}
const userId =
typeof orderData.userId === "string" ? orderData.userId.trim() : "";
const amount = normalizeAmount(orderData.amount);
if (!userId) {
await orderRef.set(
{
status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(),
failureReason: "USER_NOT_FOUND",
},
{ merge: true },
);
return;
}
if (amount === null || amount === 0) {
await orderRef.set(
{
status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(),
failureReason: "INVALID_AMOUNT",
},
{ merge: true },
);
return;
}
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(userId);
try {
await admin.firestore().runTransaction(async (transaction) => {
const userSnapshot = await transaction.get(userRef);
const userData = userSnapshot?.data() || {};
const currentBalanceValue = normalizeAmount(userData?.coins);
const currentBalance =
currentBalanceValue !== null ? currentBalanceValue : 0;
const nextBalance = currentBalance + amount;
if (
amount < 0 &&
nextBalance < 0 &&
orderData?.type === ORDER_TYPES.SONG
) {
transaction.set(
orderRef,
{
status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(),
failureReason: "INSUFFICIENT_FUNDS",
balanceBefore: currentBalance,
balanceAfter: currentBalance,
},
{ merge: true },
);
return;
}
if (userSnapshot?.exists) {
transaction.update(userRef, {
coins: nextBalance,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
} else {
transaction.set(
userRef,
{
coins: nextBalance,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
}
transaction.set(
orderRef,
{
status: ORDER_STATUS.APPLIED,
processedAt: admin.firestore.FieldValue.serverTimestamp(),
balanceBefore: currentBalance,
balanceAfter: nextBalance,
},
{ merge: true },
);
});
} catch (error) {
console.error(
"[orders-onOrderCreated] Failed to process order",
orderRef.id,
error,
);
await orderRef.set(
{
status: ORDER_STATUS.REJECTED,
processedAt: admin.firestore.FieldValue.serverTimestamp(),
failureReason: "PROCESSING_ERROR",
errorMessage: error?.message || String(error),
},
{ merge: true },
);
}
},
);
const createSongOrder = onCall({ region: REGION }, async (request) => {
const { auth, data } = request || {};
if (!auth?.uid) {
throw new HttpsError("unauthenticated", "Authentification requise.");
}
const amount = normalizeAmount(data?.amount);
if (amount === null || amount >= 0) {
throw new HttpsError(
"invalid-argument",
"Le montant doit être négatif pour un achat de musique.",
);
}
const songId =
typeof data?.songId === "string" && data.songId.trim()
? data.songId.trim()
: null;
const userRef = admin.firestore().collection(USERS_COLLECTION).doc(auth.uid);
const userSnapshot = await userRef.get();
const currentCoinsValue = normalizeAmount(userSnapshot?.data()?.coins);
const currentCoins =
currentCoinsValue !== null ? currentCoinsValue : 0;
if (currentCoins + amount < 0) {
throw new HttpsError(
"failed-precondition",
"Crédits insuffisants pour finaliser l'opération.",
);
}
const { orderId } = await createOrderDocument({
userId: auth.uid,
type: ORDER_TYPES.SONG,
amount,
songId,
createdBy: auth.uid,
metadata: {
source: data?.source || "music_generation",
requestId:
typeof data?.requestId === "string" ? data.requestId : undefined,
},
});
return { orderId };
});
module.exports = {
onOrderCreated,
createSongOrder,
};
+46 -31
View File
@@ -4,6 +4,7 @@ const { onRequest } = require("firebase-functions/v2/https");
const { FieldValue } = require("firebase-admin/firestore");
const { refList } = require("../index");
const refsList = refList;
const { ORDER_TYPES, createOrderDocument } = require("./helpers/orders");
const {
getStripeClient,
buildCheckoutLineItems,
@@ -76,9 +77,7 @@ const COIN_PACK_PRODUCTS = [
},
];
const COIN_PACK_PRODUCT_IDS = COIN_PACK_PRODUCTS.map(
(pack) => pack.productId,
);
const COIN_PACK_PRODUCT_IDS = COIN_PACK_PRODUCTS.map((pack) => pack.productId);
const COIN_PACK_PRODUCT_MAP = COIN_PACK_PRODUCTS.reduce(
(acc, pack) => ({
@@ -324,7 +323,7 @@ const handleCheckoutSessionCompleted = async (session, event) => {
lastEventAt: getServerTimestamp(),
});
const { userRef } = await resolveUserContext({
const { uid, userRef } = await resolveUserContext({
metadata: session.metadata,
customerId: session.customer,
});
@@ -378,17 +377,23 @@ const handleCheckoutSessionCompleted = async (session, event) => {
}
const alreadyGranted = Boolean(
paymentSnapshot?.exists &&
paymentSnapshot.data()?.coinPackGrantedAt,
paymentSnapshot?.exists && paymentSnapshot.data()?.coinPackGrantedAt,
);
if (!alreadyGranted) {
await userRef.set(
{
coins: FieldValue.increment(coinAmount),
const targetUserId = uid || firebaseUid || userRef.id;
await createOrderDocument({
userId: targetUserId,
type: ORDER_TYPES.COINS,
amount: coinAmount,
metadata: {
source: "STRIPE_CHECKOUT",
paymentId: session.id || null,
coinPackKey: session.metadata?.coinPackKey || null,
},
{ merge: true },
);
orderId: `stripe_${session.id}`,
});
await paymentDocRef.set(
{
@@ -409,9 +414,26 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
}
const subscriptionPayload = buildSubscriptionPayload(subscription);
if (!subscriptionPayload) {
return;
}
const { uid, userRef } = await resolveUserContext({
metadata: subscription.metadata,
customerId: subscription.customer,
});
let resolvedCustomerId = subscriptionPayload?.customerId || null;
if (!resolvedCustomerId && subscription.customer) {
resolvedCustomerId = subscription.customer;
}
const fallbackUid = extractFirebaseUid(subscription.metadata);
const resolvedUid = uid || fallbackUid || null;
await upsertPaymentDocument(subscription.id, {
customerId: subscription.customer || null,
userId: resolvedUid,
customerId: resolvedCustomerId,
subscriptionId: subscription.id || null,
status: subscription.status || null,
mode: "subscription",
@@ -420,17 +442,15 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
cancelAtPeriodEnd: subscriptionPayload?.cancelAtPeriodEnd ?? null,
currentPeriodStart: subscriptionPayload?.currentPeriodStart || null,
currentPeriodEnd: subscriptionPayload?.currentPeriodEnd || null,
metadata: subscription.metadata || {},
metadata: {
...subscription.metadata,
stripeEventType: event?.type || null,
},
lastEventType: event?.type || null,
lastEventId: event?.id || null,
lastEventAt: getServerTimestamp(),
});
const { uid, userRef } = await resolveUserContext({
metadata: subscription.metadata,
customerId: subscription.customer,
});
if (!userRef) {
console.warn(
"[subscription-handleCustomerSubscriptionEvent] User not resolved",
@@ -441,13 +461,11 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
}
const lastEvent = buildEventSnapshot(event?.type, subscription.id);
if (uid) {
lastEvent.uid = uid;
if (resolvedUid) {
lastEvent.uid = resolvedUid;
}
const priceMeta = getSubscriptionMetaFromPrice(
subscriptionPayload?.priceId,
);
const priceMeta = getSubscriptionMetaFromPrice(subscriptionPayload?.priceId);
const metadataLevel = subscription.metadata?.subscriptionLevel || null;
const metadataPeriod =
subscription.metadata?.subscriptionBillingPeriod || null;
@@ -468,6 +486,8 @@ const handleCustomerSubscriptionEvent = async (subscription, event) => {
if (subscriptionPayload?.customerId) {
userUpdate.stripeCustomerId = subscriptionPayload.customerId;
} else if (resolvedCustomerId) {
userUpdate.stripeCustomerId = resolvedCustomerId;
}
if (resolvedLevel && isPremium) {
@@ -733,9 +753,7 @@ const formatCoinPack = ({ product, price, fallback = {} }) => {
const priceId =
(resolvedPrice && resolvedPrice.id) ||
(typeof product.default_price === "string"
? product.default_price
: null);
(typeof product.default_price === "string" ? product.default_price : null);
const rawCoinAmount =
product?.metadata?.coins ??
@@ -753,8 +771,7 @@ const formatCoinPack = ({ product, price, fallback = {} }) => {
priceId,
name: product.name || "",
description: product.description || "",
coinAmount:
Number.isFinite(coinAmount) && coinAmount > 0 ? coinAmount : 0,
coinAmount: Number.isFinite(coinAmount) && coinAmount > 0 ? coinAmount : 0,
currency:
resolvedPrice?.currency ||
(typeof resolvedPrice?.currency === "string"
@@ -824,9 +841,7 @@ const listCoinPacks = onCall({ region: REGION }, async () => {
let resolvedPrice = null;
if (typeof product?.default_price === "string") {
resolvedPrice = await stripe.prices.retrieve(
product.default_price,
);
resolvedPrice = await stripe.prices.retrieve(product.default_price);
} else if (
product?.default_price &&
typeof product.default_price === "object"
+10 -7
View File
@@ -4,6 +4,7 @@ const {
onDocumentCreated,
} = require("firebase-functions/firestore");
const { refList } = require("../index");
const { ORDER_TYPES, createOrderDocument } = require("./helpers/orders");
const { deleteFolder } = require("../helpers/firebase");
const { Resend } = require("resend");
const { welcomeTemplate } = require("../helpers/email");
@@ -56,14 +57,16 @@ exports.onUserCreated = onDocumentCreated("users/{userID}", async (event) => {
lastName = "",
} = event?.data?.data() || {};
const userId = event?.params?.userID;
try {
await event.data.ref.set(
{
coins: admin.firestore.FieldValue.increment(10),
coinWelcomeGrantedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
);
await createOrderDocument({
userId,
type: ORDER_TYPES.GIFT,
amount: 10,
metadata: { reason: "WELCOME_BONUS" },
orderId: `welcome_${userId}`,
});
} catch (coinError) {
console.warn(
"[users-onUserCreated] Unable to grant welcome coins",