subscription.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { z } from "zod"
  2. import { fn } from "./util/fn"
  3. import { centsToMicroCents } from "./util/price"
  4. import { getWeekBounds, getMonthlyBounds } from "./util/date"
  5. export namespace Subscription {
  6. export const analyzeRollingUsage = fn(
  7. z.object({
  8. limit: z.number().int(),
  9. window: z.number().int(),
  10. usage: z.number().int(),
  11. timeUpdated: z.date(),
  12. }),
  13. ({ limit, window, usage, timeUpdated }) => {
  14. const now = new Date()
  15. const rollingWindowMs = window * 3600 * 1000
  16. const rollingLimitInMicroCents = centsToMicroCents(limit * 100)
  17. const windowStart = new Date(now.getTime() - rollingWindowMs)
  18. if (timeUpdated < windowStart) {
  19. return {
  20. status: "ok" as const,
  21. resetInSec: window * 3600,
  22. usagePercent: 0,
  23. }
  24. }
  25. const windowEnd = new Date(timeUpdated.getTime() + rollingWindowMs)
  26. if (usage < rollingLimitInMicroCents) {
  27. return {
  28. status: "ok" as const,
  29. resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
  30. usagePercent: Math.floor(Math.min(100, (usage / rollingLimitInMicroCents) * 100)),
  31. }
  32. }
  33. return {
  34. status: "rate-limited" as const,
  35. resetInSec: Math.ceil((windowEnd.getTime() - now.getTime()) / 1000),
  36. usagePercent: 100,
  37. }
  38. },
  39. )
  40. export const analyzeWeeklyUsage = fn(
  41. z.object({
  42. limit: z.number().int(),
  43. usage: z.number().int(),
  44. timeUpdated: z.date(),
  45. }),
  46. ({ limit, usage, timeUpdated }) => {
  47. const now = new Date()
  48. const week = getWeekBounds(now)
  49. const fixedLimitInMicroCents = centsToMicroCents(limit * 100)
  50. if (timeUpdated < week.start) {
  51. return {
  52. status: "ok" as const,
  53. resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
  54. usagePercent: 0,
  55. }
  56. }
  57. if (usage < fixedLimitInMicroCents) {
  58. return {
  59. status: "ok" as const,
  60. resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
  61. usagePercent: Math.floor(Math.min(100, (usage / fixedLimitInMicroCents) * 100)),
  62. }
  63. }
  64. return {
  65. status: "rate-limited" as const,
  66. resetInSec: Math.ceil((week.end.getTime() - now.getTime()) / 1000),
  67. usagePercent: 100,
  68. }
  69. },
  70. )
  71. export const analyzeMonthlyUsage = fn(
  72. z.object({
  73. limit: z.number().int(),
  74. usage: z.number().int(),
  75. timeUpdated: z.date(),
  76. timeSubscribed: z.date(),
  77. }),
  78. ({ limit, usage, timeUpdated, timeSubscribed }) => {
  79. const now = new Date()
  80. const month = getMonthlyBounds(now, timeSubscribed)
  81. const fixedLimitInMicroCents = centsToMicroCents(limit * 100)
  82. if (timeUpdated < month.start) {
  83. return {
  84. status: "ok" as const,
  85. resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000),
  86. usagePercent: 0,
  87. }
  88. }
  89. if (usage < fixedLimitInMicroCents) {
  90. return {
  91. status: "ok" as const,
  92. resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000),
  93. usagePercent: Math.floor(Math.min(100, (usage / fixedLimitInMicroCents) * 100)),
  94. }
  95. }
  96. return {
  97. status: "rate-limited" as const,
  98. resetInSec: Math.ceil((month.end.getTime() - now.getTime()) / 1000),
  99. usagePercent: 100,
  100. }
  101. },
  102. )
  103. }