diff --git a/.env.example b/.env.example index 0ff47a8580ef6c..ff2e2c1875f4a0 100644 --- a/.env.example +++ b/.env.example @@ -343,12 +343,14 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0 APP_ROUTER_APPS_CATEGORIES_ENABLED=0 # whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0 +APP_ROUTER_BOOKING_ENABLED=0 APP_ROUTER_BOOKINGS_STATUS_ENABLED=0 APP_ROUTER_WORKFLOWS_ENABLED=0 APP_ROUTER_SETTINGS_TEAMS_ENABLED=0 APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0 APP_ROUTER_APPS_ENABLED=0 APP_ROUTER_VIDEO_ENABLED=0 +APP_ROUTER_TEAM_ENABLED=0 APP_ROUTER_TEAMS_ENABLED=0 APP_ROUTER_AVAILABILITY_ENABLED=0 APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0 @@ -392,3 +394,7 @@ VAPID_PRIVATE_KEY= # Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs) NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL= NEXT_PUBLIC_WEBSITE_TERMS_URL= + +# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs. +# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal] +NEXT_PUBLIC_LOGGER_LEVEL= diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 33e187df6d2a9f..a071344ccd1e59 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -49,7 +49,9 @@ assignees: "" (Share it here.) --- + ##### House rules + - If this issue has a `🚨 needs approval` label, don't start coding yet. Wait until a core member approves feature request by removing this label, then you can start coding. - For clarity: Non-core member issues automatically get the `🚨 needs approval` label. - Your feature ideas are invaluable to us! However, they undergo review to ensure alignment with the product's direction. diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index 91682e2fd6bbc3..dcd7c483228498 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -15,7 +15,7 @@ env: IS_E2E: true NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=29000 REDIS_URL: "redis://localhost:6379" STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a3c94d605590ca..66600c40a00818 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -7,14 +7,14 @@ jobs: test: name: Unit timeout-minutes: 20 - runs-on: buildjet-2vcpu-ubuntu-2204 + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - - run: yarn test + - run: yarn test -- --no-isolate # We could add different timezones here that we need to run our tests in - - run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly + - run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly --no-isolate - name: Run API v2 tests working-directory: apps/api/v2 run: | diff --git a/.yarn/patches/@prisma-client-npm-5.4.2-fca489b2dc.patch b/.yarn/patches/@prisma-client-npm-5.4.2-fca489b2dc.patch new file mode 100644 index 00000000000000..93d2ea935d6591 --- /dev/null +++ b/.yarn/patches/@prisma-client-npm-5.4.2-fca489b2dc.patch @@ -0,0 +1,26 @@ +diff --git a/runtime/binary.js b/runtime/binary.js +index c81267d752644043e97b35d26369a5ce266abfd0..034c11262143d1acbf9902745a441d3b0cdfc0fd 100644 +--- a/runtime/binary.js ++++ b/runtime/binary.js +@@ -185,7 +185,7 @@ It should have this form: { url: "CONNECTION_STRING" }`);if(r&&typeof r=="object + It should have this form: { url: "CONNECTION_STRING" }`);if(typeof i!="string")throw new Me(`Invalid value ${JSON.stringify(i)} for datasource "${t}" provided to PrismaClient constructor. + It should have this form: { url: "CONNECTION_STRING" }`)}}}},adapter:(e,A)=>{if(e===null)return;if(e===void 0)throw new Me('"adapter" property must not be undefined, use null to conditionally disable driver adapters.');if(!cg(A).includes("driverAdapters"))throw new Me('"adapter" property can only be provided to PrismaClient constructor when "driverAdapters" preview feature is enabled.')},datasourceUrl:e=>{if(typeof e<"u"&&typeof e!="string")throw new Me(`Invalid value ${JSON.stringify(e)} for "datasourceUrl" provided to PrismaClient constructor. + Expected string or undefined.`)},errorFormat:e=>{if(e){if(typeof e!="string")throw new Me(`Invalid value ${JSON.stringify(e)} for "errorFormat" provided to PrismaClient constructor.`);if(!PR.includes(e)){let A=Ni(e,PR);throw new Me(`Invalid errorFormat ${e} provided to PrismaClient constructor.${A}`)}}},log:e=>{if(!e)return;if(!Array.isArray(e))throw new Me(`Invalid value ${JSON.stringify(e)} for "log" provided to PrismaClient constructor.`);function A(t){if(typeof t=="string"&&!GR.includes(t)){let r=Ni(t,GR);throw new Me(`Invalid log level "${t}" provided to PrismaClient constructor.${r}`)}}for(let t of e){A(t);let r={level:A,emit:n=>{let i=["stdout","event"];if(!i.includes(n)){let s=Ni(n,i);throw new Me(`Invalid value ${JSON.stringify(n)} for "emit" in logLevel provided to PrismaClient constructor.${s}`)}}};if(t&&typeof t=="object")for(let[n,i]of Object.entries(t))if(r[n])r[n](i);else throw new Me(`Invalid property ${n} for "log" provided to PrismaClient constructor`)}},__internal:e=>{if(!e)return;let A=["debug","hooks","engine","measurePerformance"];if(typeof e!="object")throw new Me(`Invalid value ${JSON.stringify(e)} for "__internal" to PrismaClient constructor`);for(let[t]of Object.entries(e))if(!A.includes(t)){let r=Ni(t,A);throw new Me(`Invalid property ${JSON.stringify(t)} for "__internal" provided to PrismaClient constructor.${r}`)}}};function YR(e,A){for(let[t,r]of Object.entries(e)){if(!vR.includes(t)){let n=Ni(t,vR);throw new Me(`Unknown property ${t} provided to PrismaClient constructor.${n}`)}VJ[t](r,A)}if(e.datasourceUrl&&e.datasources)throw new Me('Can not use "datasourceUrl" and "datasources" options at the same time. Pick one of them')}function Ni(e,A){if(A.length===0||typeof e!="string")return"";let t=qJ(e,A);return t?` Did you mean "${t}"?`:""}function qJ(e,A){if(A.length===0)return null;let t=A.map(n=>({value:n,distance:(0,JR.default)(e,n)}));t.sort((n,i)=>n.distance{let r=new Array(e.length),n=null,i=!1,s=0,o=()=>{i||(s++,s===e.length&&(i=!0,n?t(n):A(r)))},a=c=>{i||(i=!0,t(c))};for(let c=0;c{r[c]=g,o()},g=>{if(!Cg(g)){a(g);return}g.batchRequestIdx===c?a(g):(n||(n=g),o())})})}var pr=ce("prisma:client");typeof globalThis=="object"&&(globalThis.NODE_CLIENT=!0);var OJ={requestArgsToMiddlewareArgs:e=>e,middlewareArgsToRequestArgs:e=>e},HJ=Symbol.for("prisma.client.transaction.id"),WJ={id:0,nextId(){return++this.id}};function _R(e){class A{constructor(r){this._middlewares=new dg;this._createPrismaPromise=ed();this.$extends=Xf;EI(e),r&&YR(r,e);let n=r?.adapter?cf(r.adapter):void 0,i=new HR.EventEmitter().on("error",()=>{});this._extensions=wa.empty(),this._previewFeatures=cg(e),this._clientVersion=e.clientVersion??TR,this._activeProvider=e.activeProvider,this._tracingHelper=DR(this._previewFeatures);let s={rootEnvPath:e.relativeEnvPaths.rootEnvPath&&Qo.default.resolve(e.dirname,e.relativeEnvPaths.rootEnvPath),schemaEnvPath:e.relativeEnvPaths.schemaEnvPath&&Qo.default.resolve(e.dirname,e.relativeEnvPaths.schemaEnvPath)},o=!n&&qi(s,{conflictCheck:"none"})||e.injectableEdgeEnv?.();try{let a=r??{},c=a.__internal??{},g=c.debug===!0;g&&ce.enable("prisma:client");let l=Qo.default.resolve(e.dirname,e.relativePath);WR.default.existsSync(l)||(l=e.dirname),pr("dirname",e.dirname),pr("relativePath",e.relativePath),pr("cwd",l);let u=c.engine||{};if(a.errorFormat?this._errorFormat=a.errorFormat:process.env.NODE_ENV==="production"?this._errorFormat="minimal":process.env.NO_COLOR?this._errorFormat="colorless":this._errorFormat="colorless",this._runtimeDataModel=e.runtimeDataModel,this._engineConfig={cwd:l,dirname:e.dirname,enableDebugLogs:g,allowTriggerPanic:u.allowTriggerPanic,datamodelPath:Qo.default.join(e.dirname,e.filename??"schema.prisma"),prismaPath:u.binaryPath??void 0,engineEndpoint:u.endpoint,generator:e.generator,showColors:this._errorFormat==="pretty",logLevel:a.log&&kR(a.log),logQueries:a.log&&!!(typeof a.log=="string"?a.log==="query":a.log.find(E=>typeof E=="string"?E==="query":E.level==="query")),env:o?.parsed??{},flags:[],clientVersion:e.clientVersion,engineVersion:e.engineVersion,previewFeatures:this._previewFeatures,activeProvider:e.activeProvider,inlineSchema:e.inlineSchema,overrideDatasources:hI(a,e.datasourceNames),inlineDatasources:e.inlineDatasources,inlineSchemaHash:e.inlineSchemaHash,tracingHelper:this._tracingHelper,logEmitter:i,isBundled:e.isBundled,adapter:n},pr("clientVersion",e.clientVersion),this._engine=tR(e,this._engineConfig),this._requestHandler=new Ig(this,i),a.log)for(let E of a.log){let h=typeof E=="string"?E:E.emit==="stdout"?E.level:null;h&&this.$on(h,d=>{Wi.log(`${Wi.tags[h]??""}`,d.message||d.query)})}this._metrics=new yn(this._engine)}catch(a){throw a.clientVersion=this._clientVersion,a}return this._appliedParent=is(this)}get[Symbol.toStringTag](){return"PrismaClient"}$use(r){this._middlewares.use(r)}$on(r,n){r==="beforeExit"?this._engine.on("beforeExit",n):this._engine.on(r,i=>{let s=i.fields;return n(r==="query"?{timestamp:i.timestamp,query:s?.query??i.query,params:s?.params??i.params,duration:s?.duration_ms??i.duration,target:i.target}:{timestamp:i.timestamp,message:s?.message??i.message,target:i.target})})}$connect(){try{return this._engine.start()}catch(r){throw r.clientVersion=this._clientVersion,r}}async $disconnect(){try{await this._engine.stop()}catch(r){throw r.clientVersion=this._clientVersion,r}finally{Rd()}}$executeRawInternal(r,n,i,s){let o=this._activeProvider,a=this._engineConfig.adapter?.flavour;return this._request({action:"executeRaw",args:i,transaction:r,clientMethod:n,argsMapper:$h({clientMethod:n,activeProvider:o,activeProviderFlavour:a}),callsite:nr(this._errorFormat),dataPath:[],middlewareArgsMapper:s})}$executeRaw(r,...n){return this._createPrismaPromise(i=>{if(r.raw!==void 0||r.sql!==void 0){let[s,o]=qR(r,n);return zh(this._activeProvider,s.text,s.values,Array.isArray(r)?"prisma.$executeRaw``":"prisma.$executeRaw(sql``)"),this.$executeRawInternal(i,"$executeRaw",s,o)}throw new AA("`$executeRaw` is a tag function, please use it like the following:\n```\nconst result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`\n```\n\nOr read our docs at https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#executeraw\n",{clientVersion:this._clientVersion})})}$executeRawUnsafe(r,...n){return this._createPrismaPromise(i=>(zh(this._activeProvider,r,n,"prisma.$executeRawUnsafe(, [...values])"),this.$executeRawInternal(i,"$executeRawUnsafe",[r,...n])))}$runCommandRaw(r){if(e.activeProvider!=="mongodb")throw new AA(`The ${e.activeProvider} provider does not support $runCommandRaw. Use the mongodb provider.`,{clientVersion:this._clientVersion});return this._createPrismaPromise(n=>this._request({args:r,clientMethod:"$runCommandRaw",dataPath:[],action:"runCommandRaw",argsMapper:CR,callsite:nr(this._errorFormat),transaction:n}))}async $queryRawInternal(r,n,i,s){let o=this._activeProvider,a=this._engineConfig.adapter?.flavour;return this._request({action:"queryRaw",args:i,transaction:r,clientMethod:n,argsMapper:$h({clientMethod:n,activeProvider:o,activeProviderFlavour:a}),callsite:nr(this._errorFormat),dataPath:[],middlewareArgsMapper:s}).then(LR)}$queryRaw(r,...n){return this._createPrismaPromise(i=>{if(r.raw!==void 0||r.sql!==void 0)return this.$queryRawInternal(i,"$queryRaw",...qR(r,n));throw new AA("`$queryRaw` is a tag function, please use it like the following:\n```\nconst result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`\n```\n\nOr read our docs at https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#queryraw\n",{clientVersion:this._clientVersion})})}$queryRawUnsafe(r,...n){return this._createPrismaPromise(i=>this.$queryRawInternal(i,"$queryRawUnsafe",[r,...n]))}_transactionWithArray({promises:r,options:n}){let i=WJ.nextId(),s=bR(r.length),o=r.map((a,c)=>{if(a?.[Symbol.toStringTag]!=="PrismaPromise")throw new Error("All elements of the array need to be Prisma Client promises. Hint: Please make sure you are not awaiting the Prisma client calls you intended to pass in the $transaction function.");let g=n?.isolationLevel,l={kind:"batch",id:i,index:c,isolationLevel:g,lock:s};return a.requestTransaction?.(l)??a});return VR(o)}async _transactionWithCallback({callback:r,options:n}){let i={traceparent:this._tracingHelper.getTraceParent()},s=await this._engine.transaction("start",i,n),o;try{let a={kind:"itx",...s};o=await r(this._createItxClient(a)),await this._engine.transaction("commit",i,s)}catch(a){throw await this._engine.transaction("rollback",i,s).catch(()=>{}),a}return o}_createItxClient(r){return is(Et(pa(this),[aA("_appliedParent",()=>this._appliedParent._createItxClient(r)),aA("_createPrismaPromise",()=>ed(r)),aA(HJ,()=>r.id),As(td)]))}$transaction(r,n){let i;typeof r=="function"?i=()=>this._transactionWithCallback({callback:r,options:n}):i=()=>this._transactionWithArray({promises:r,options:n});let s={name:"transaction",attributes:{method:"$transaction"}};return this._tracingHelper.runInChildSpan(s,i)}_request(r){r.otelParentCtx=this._tracingHelper.getActiveContext();let n=r.middlewareArgsMapper??OJ,i={args:n.requestArgsToMiddlewareArgs(r.args),dataPath:r.dataPath,runInTransaction:!!r.transaction,action:r.action,model:r.model},s={middleware:{name:"middleware",middleware:!0,attributes:{method:"$use"},active:!1},operation:{name:"operation",attributes:{method:i.action,model:i.model,name:i.model?`${i.model}.${i.action}`:i.action}}},o=-1,a=async c=>{let g=this._middlewares.get(++o);if(g)return this._tracingHelper.runInChildSpan(s.middleware,Q=>g(c,I=>(Q?.end(),a(I))));let{runInTransaction:l,args:u,...E}=c,h={...r,...E};u&&(h.args=n.middlewareArgsToRequestArgs(u)),r.transaction!==void 0&&l===!1&&delete h.transaction;let d=await nI(this,h);return h.model?eI({result:d,modelName:h.model,args:h.args,extensions:this._extensions,runtimeDataModel:this._runtimeDataModel}):d};return this._tracingHelper.runInChildSpan(s.operation,()=>new OR.AsyncResource("prisma-client-request").runInAsyncScope(()=>a(i)))}async _executeRequest({args:r,clientMethod:n,dataPath:i,callsite:s,action:o,model:a,argsMapper:c,transaction:g,unpacker:l,otelParentCtx:u,customDataProxyFetch:E}){try{r=c?c(r):r;let h={name:"serialize"},d=this._tracingHelper.runInChildSpan(h,()=>ER({modelName:a,runtimeDataModel:this._runtimeDataModel,action:o,args:r,clientMethod:n,callsite:s,extensions:this._extensions,errorFormat:this._errorFormat,clientVersion:this._clientVersion}));return ce.enabled("prisma:client")&&(pr("Prisma Client call:"),pr(`prisma.${n}(${Mf(r)})`),pr("Generated request:"),pr(JSON.stringify(d,null,2)+` +-`)),g?.kind==="batch"&&await g.lock,this._requestHandler.request({protocolQuery:d,modelName:a,action:o,clientMethod:n,dataPath:i,callsite:s,args:r,extensions:this._extensions,transaction:g,unpacker:l,otelParentCtx:u,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:E})}catch(h){throw h.clientVersion=this._clientVersion,h}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new AA("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(r){return!!this._engineConfig.previewFeatures?.includes(r)}}return A}function qR(e,A){return _J(e)?[new QA(e,A),mR]:[e,yR]}function _J(e){return Array.isArray(e)&&Array.isArray(e.raw)}var KJ=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function KR(e){return new Proxy(e,{get(A,t){if(t in A)return A[t];if(!KJ.has(t))throw new TypeError(`Invalid enum value: ${String(t)}`)}})}function jR(e){qi(e,{conflictCheck:"warn"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce}); ++`)),g?.kind==="batch"&&await g.lock,this._requestHandler.request({protocolQuery:d,modelName:a,action:o,clientMethod:n,dataPath:i,callsite:s,args:r,extensions:this._extensions,transaction:g,unpacker:l,otelParentCtx:u,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:E})}catch(h){throw h.clientVersion=this._clientVersion,h}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new AA("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(r){return!!this._engineConfig.previewFeatures?.includes(r)}}return A}function qR(e,A){return _J(e)?[new QA(e,A),mR]:[e,yR]}function _J(e){return Array.isArray(e)&&Array.isArray(e.raw)}var KJ=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function KR(e){return new Proxy(e,{get(A,t){if(t in A)return A[t];if(!KJ.has(t))throw new TypeError(`Invalid enum value: ${String(t)}`)}})}function jR(e){qi(e,{conflictCheck:"none"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce}); + /*! Bundled license information: + + undici/lib/fetch/body.js: +diff --git a/runtime/library.js b/runtime/library.js +index 65b30894c697f97a54924dcf7acc4b7e45002f4d..b2d26124f34759bb222a08e76bcc0f6d52b3c573 100644 +--- a/runtime/library.js ++++ b/runtime/library.js +@@ -126,7 +126,7 @@ It should have this form: { url: "CONNECTION_STRING" }`);if(n&&typeof n=="object + It should have this form: { url: "CONNECTION_STRING" }`);if(typeof o!="string")throw new q(`Invalid value ${JSON.stringify(o)} for datasource "${r}" provided to PrismaClient constructor. + It should have this form: { url: "CONNECTION_STRING" }`)}}}},adapter:(e,t)=>{if(e===null)return;if(e===void 0)throw new q('"adapter" property must not be undefined, use null to conditionally disable driver adapters.');if(!gn(t).includes("driverAdapters"))throw new q('"adapter" property can only be provided to PrismaClient constructor when "driverAdapters" preview feature is enabled.')},datasourceUrl:e=>{if(typeof e<"u"&&typeof e!="string")throw new q(`Invalid value ${JSON.stringify(e)} for "datasourceUrl" provided to PrismaClient constructor. + Expected string or undefined.`)},errorFormat:e=>{if(e){if(typeof e!="string")throw new q(`Invalid value ${JSON.stringify(e)} for "errorFormat" provided to PrismaClient constructor.`);if(!Cl.includes(e)){let t=Rt(e,Cl);throw new q(`Invalid errorFormat ${e} provided to PrismaClient constructor.${t}`)}}},log:e=>{if(!e)return;if(!Array.isArray(e))throw new q(`Invalid value ${JSON.stringify(e)} for "log" provided to PrismaClient constructor.`);function t(r){if(typeof r=="string"&&!Al.includes(r)){let n=Rt(r,Al);throw new q(`Invalid log level "${r}" provided to PrismaClient constructor.${n}`)}}for(let r of e){t(r);let n={level:t,emit:i=>{let o=["stdout","event"];if(!o.includes(i)){let s=Rt(i,o);throw new q(`Invalid value ${JSON.stringify(i)} for "emit" in logLevel provided to PrismaClient constructor.${s}`)}}};if(r&&typeof r=="object")for(let[i,o]of Object.entries(r))if(n[i])n[i](o);else throw new q(`Invalid property ${i} for "log" provided to PrismaClient constructor`)}},__internal:e=>{if(!e)return;let t=["debug","hooks","engine","measurePerformance"];if(typeof e!="object")throw new q(`Invalid value ${JSON.stringify(e)} for "__internal" to PrismaClient constructor`);for(let[r]of Object.entries(e))if(!t.includes(r)){let n=Rt(r,t);throw new q(`Invalid property ${JSON.stringify(r)} for "__internal" provided to PrismaClient constructor.${n}`)}}};function Ml(e,t){for(let[r,n]of Object.entries(e)){if(!Tl.includes(r)){let i=Rt(r,Tl);throw new q(`Unknown property ${r} provided to PrismaClient constructor.${i}`)}Xd[r](n,t)}if(e.datasourceUrl&&e.datasources)throw new q('Can not use "datasourceUrl" and "datasources" options at the same time. Pick one of them')}function Rt(e,t){if(t.length===0||typeof e!="string")return"";let r=em(e,t);return r?` Did you mean "${r}"?`:""}function em(e,t){if(t.length===0)return null;let r=t.map(i=>({value:i,distance:(0,Rl.default)(e,i)}));r.sort((i,o)=>i.distance{let n=new Array(e.length),i=null,o=!1,s=0,a=()=>{o||(s++,s===e.length&&(o=!0,i?r(i):t(n)))},l=u=>{o||(o=!0,r(u))};for(let u=0;u{n[u]=c,a()},c=>{if(!Pn(c)){l(c);return}c.batchRequestIdx===u?l(c):(i||(i=c),a())})})}var Ue=D("prisma:client");typeof globalThis=="object"&&(globalThis.NODE_CLIENT=!0);var tm={requestArgsToMiddlewareArgs:e=>e,middlewareArgsToRequestArgs:e=>e},rm=Symbol.for("prisma.client.transaction.id"),nm={id:0,nextId(){return++this.id}};function Dl(e){class t{constructor(n){this._middlewares=new wn;this._createPrismaPromise=Hi();this.$extends=ra;xa(e),n&&Ml(n,e);let i=n?.adapter?fs(n.adapter):void 0,o=new Fl.EventEmitter().on("error",()=>{});this._extensions=ln.empty(),this._previewFeatures=gn(e),this._clientVersion=e.clientVersion??wl,this._activeProvider=e.activeProvider,this._tracingHelper=ml(this._previewFeatures);let s={rootEnvPath:e.relativeEnvPaths.rootEnvPath&&yr.default.resolve(e.dirname,e.relativeEnvPaths.rootEnvPath),schemaEnvPath:e.relativeEnvPaths.schemaEnvPath&&yr.default.resolve(e.dirname,e.relativeEnvPaths.schemaEnvPath)},a=!i&&_t(s,{conflictCheck:"none"})||e.injectableEdgeEnv?.();try{let l=n??{},u=l.__internal??{},c=u.debug===!0;c&&D.enable("prisma:client");let p=yr.default.resolve(e.dirname,e.relativePath);Ol.default.existsSync(p)||(p=e.dirname),Ue("dirname",e.dirname),Ue("relativePath",e.relativePath),Ue("cwd",p);let d=u.engine||{};if(l.errorFormat?this._errorFormat=l.errorFormat:process.env.NODE_ENV==="production"?this._errorFormat="minimal":process.env.NO_COLOR?this._errorFormat="colorless":this._errorFormat="colorless",this._runtimeDataModel=e.runtimeDataModel,this._engineConfig={cwd:p,dirname:e.dirname,enableDebugLogs:c,allowTriggerPanic:d.allowTriggerPanic,datamodelPath:yr.default.join(e.dirname,e.filename??"schema.prisma"),prismaPath:d.binaryPath??void 0,engineEndpoint:d.endpoint,generator:e.generator,showColors:this._errorFormat==="pretty",logLevel:l.log&&gl(l.log),logQueries:l.log&&!!(typeof l.log=="string"?l.log==="query":l.log.find(f=>typeof f=="string"?f==="query":f.level==="query")),env:a?.parsed??{},flags:[],clientVersion:e.clientVersion,engineVersion:e.engineVersion,previewFeatures:this._previewFeatures,activeProvider:e.activeProvider,inlineSchema:e.inlineSchema,overrideDatasources:ba(l,e.datasourceNames),inlineDatasources:e.inlineDatasources,inlineSchemaHash:e.inlineSchemaHash,tracingHelper:this._tracingHelper,logEmitter:o,isBundled:e.isBundled,adapter:i},Ue("clientVersion",e.clientVersion),this._engine=Ua(e,this._engineConfig),this._requestHandler=new Cn(this,o),l.log)for(let f of l.log){let y=typeof f=="string"?f:f.emit==="stdout"?f.level:null;y&&this.$on(y,g=>{$t.log(`${$t.tags[y]??""}`,g.message||g.query)})}this._metrics=new dt(this._engine)}catch(l){throw l.clientVersion=this._clientVersion,l}return this._appliedParent=zt(this)}get[Symbol.toStringTag](){return"PrismaClient"}$use(n){this._middlewares.use(n)}$on(n,i){n==="beforeExit"?this._engine.on("beforeExit",i):this._engine.on(n,o=>{let s=o.fields;return i(n==="query"?{timestamp:o.timestamp,query:s?.query??o.query,params:s?.params??o.params,duration:s?.duration_ms??o.duration,target:o.target}:{timestamp:o.timestamp,message:s?.message??o.message,target:o.target})})}$connect(){try{return this._engine.start()}catch(n){throw n.clientVersion=this._clientVersion,n}}async $disconnect(){try{await this._engine.stop()}catch(n){throw n.clientVersion=this._clientVersion,n}finally{Eo()}}$executeRawInternal(n,i,o,s){let a=this._activeProvider,l=this._engineConfig.adapter?.flavour;return this._request({action:"executeRaw",args:o,transaction:n,clientMethod:i,argsMapper:Ji({clientMethod:i,activeProvider:a,activeProviderFlavour:l}),callsite:Ve(this._errorFormat),dataPath:[],middlewareArgsMapper:s})}$executeRaw(n,...i){return this._createPrismaPromise(o=>{if(n.raw!==void 0||n.sql!==void 0){let[s,a]=Il(n,i);return Gi(this._activeProvider,s.text,s.values,Array.isArray(n)?"prisma.$executeRaw``":"prisma.$executeRaw(sql``)"),this.$executeRawInternal(o,"$executeRaw",s,a)}throw new X("`$executeRaw` is a tag function, please use it like the following:\n```\nconst result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`\n```\n\nOr read our docs at https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#executeraw\n",{clientVersion:this._clientVersion})})}$executeRawUnsafe(n,...i){return this._createPrismaPromise(o=>(Gi(this._activeProvider,n,i,"prisma.$executeRawUnsafe(, [...values])"),this.$executeRawInternal(o,"$executeRawUnsafe",[n,...i])))}$runCommandRaw(n){if(e.activeProvider!=="mongodb")throw new X(`The ${e.activeProvider} provider does not support $runCommandRaw. Use the mongodb provider.`,{clientVersion:this._clientVersion});return this._createPrismaPromise(i=>this._request({args:n,clientMethod:"$runCommandRaw",dataPath:[],action:"runCommandRaw",argsMapper:nl,callsite:Ve(this._errorFormat),transaction:i}))}async $queryRawInternal(n,i,o,s){let a=this._activeProvider,l=this._engineConfig.adapter?.flavour;return this._request({action:"queryRaw",args:o,transaction:n,clientMethod:i,argsMapper:Ji({clientMethod:i,activeProvider:a,activeProviderFlavour:l}),callsite:Ve(this._errorFormat),dataPath:[],middlewareArgsMapper:s}).then(Pl)}$queryRaw(n,...i){return this._createPrismaPromise(o=>{if(n.raw!==void 0||n.sql!==void 0)return this.$queryRawInternal(o,"$queryRaw",...Il(n,i));throw new X("`$queryRaw` is a tag function, please use it like the following:\n```\nconst result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`\n```\n\nOr read our docs at https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#queryraw\n",{clientVersion:this._clientVersion})})}$queryRawUnsafe(n,...i){return this._createPrismaPromise(o=>this.$queryRawInternal(o,"$queryRawUnsafe",[n,...i]))}_transactionWithArray({promises:n,options:i}){let o=nm.nextId(),s=fl(n.length),a=n.map((l,u)=>{if(l?.[Symbol.toStringTag]!=="PrismaPromise")throw new Error("All elements of the array need to be Prisma Client promises. Hint: Please make sure you are not awaiting the Prisma client calls you intended to pass in the $transaction function.");let c=i?.isolationLevel,p={kind:"batch",id:o,index:u,isolationLevel:c,lock:s};return l.requestTransaction?.(p)??l});return Sl(a)}async _transactionWithCallback({callback:n,options:i}){let o={traceparent:this._tracingHelper.getTraceParent()},s=await this._engine.transaction("start",o,i),a;try{let l={kind:"itx",...s};a=await n(this._createItxClient(l)),await this._engine.transaction("commit",o,s)}catch(l){throw await this._engine.transaction("rollback",o,s).catch(()=>{}),l}return a}_createItxClient(n){return zt(Ee(on(this),[re("_appliedParent",()=>this._appliedParent._createItxClient(n)),re("_createPrismaPromise",()=>Hi(n)),re(rm,()=>n.id),Gt(zi)]))}$transaction(n,i){let o;typeof n=="function"?o=()=>this._transactionWithCallback({callback:n,options:i}):o=()=>this._transactionWithArray({promises:n,options:i});let s={name:"transaction",attributes:{method:"$transaction"}};return this._tracingHelper.runInChildSpan(s,o)}_request(n){n.otelParentCtx=this._tracingHelper.getActiveContext();let i=n.middlewareArgsMapper??tm,o={args:i.requestArgsToMiddlewareArgs(n.args),dataPath:n.dataPath,runInTransaction:!!n.transaction,action:n.action,model:n.model},s={middleware:{name:"middleware",middleware:!0,attributes:{method:"$use"},active:!1},operation:{name:"operation",attributes:{method:o.action,model:o.model,name:o.model?`${o.model}.${o.action}`:o.action}}},a=-1,l=async u=>{let c=this._middlewares.get(++a);if(c)return this._tracingHelper.runInChildSpan(s.middleware,P=>c(u,T=>(P?.end(),l(T))));let{runInTransaction:p,args:d,...f}=u,y={...n,...f};d&&(y.args=i.middlewareArgsToRequestArgs(d)),n.transaction!==void 0&&p===!1&&delete y.transaction;let g=await ua(this,y);return y.model?oa({result:g,modelName:y.model,args:y.args,extensions:this._extensions,runtimeDataModel:this._runtimeDataModel}):g};return this._tracingHelper.runInChildSpan(s.operation,()=>new kl.AsyncResource("prisma-client-request").runInAsyncScope(()=>l(o)))}async _executeRequest({args:n,clientMethod:i,dataPath:o,callsite:s,action:a,model:l,argsMapper:u,transaction:c,unpacker:p,otelParentCtx:d,customDataProxyFetch:f}){try{n=u?u(n):n;let y={name:"serialize"},g=this._tracingHelper.runInChildSpan(y,()=>el({modelName:l,runtimeDataModel:this._runtimeDataModel,action:a,args:n,clientMethod:i,callsite:s,extensions:this._extensions,errorFormat:this._errorFormat,clientVersion:this._clientVersion}));return D.enabled("prisma:client")&&(Ue("Prisma Client call:"),Ue(`prisma.${i}(${$s(n)})`),Ue("Generated request:"),Ue(JSON.stringify(g,null,2)+` +-`)),c?.kind==="batch"&&await c.lock,this._requestHandler.request({protocolQuery:g,modelName:l,action:a,clientMethod:i,dataPath:o,callsite:s,args:n,extensions:this._extensions,transaction:c,unpacker:p,otelParentCtx:d,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:f})}catch(y){throw y.clientVersion=this._clientVersion,y}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new X("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(n){return!!this._engineConfig.previewFeatures?.includes(n)}}return t}function Il(e,t){return im(e)?[new oe(e,t),ul]:[e,cl]}function im(e){return Array.isArray(e)&&Array.isArray(e.raw)}var om=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function _l(e){return new Proxy(e,{get(t,r){if(r in t)return t[r];if(!om.has(r))throw new TypeError(`Invalid enum value: ${String(r)}`)}})}function Nl(e){_t(e,{conflictCheck:"warn"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce}); ++`)),c?.kind==="batch"&&await c.lock,this._requestHandler.request({protocolQuery:g,modelName:l,action:a,clientMethod:i,dataPath:o,callsite:s,args:n,extensions:this._extensions,transaction:c,unpacker:p,otelParentCtx:d,otelChildCtx:this._tracingHelper.getActiveContext(),customDataProxyFetch:f})}catch(y){throw y.clientVersion=this._clientVersion,y}}get $metrics(){if(!this._hasPreviewFlag("metrics"))throw new X("`metrics` preview feature must be enabled in order to access metrics API",{clientVersion:this._clientVersion});return this._metrics}_hasPreviewFlag(n){return!!this._engineConfig.previewFeatures?.includes(n)}}return t}function Il(e,t){return im(e)?[new oe(e,t),ul]:[e,cl]}function im(e){return Array.isArray(e)&&Array.isArray(e.raw)}var om=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function _l(e){return new Proxy(e,{get(t,r){if(r in t)return t[r];if(!om.has(r))throw new TypeError(`Invalid enum value: ${String(r)}`)}})}function Nl(e){_t(e,{conflictCheck:"none"})}0&&(module.exports={DMMF,DMMFClass,Debug,Decimal,Extensions,MetricsClient,NotFoundError,ObjectEnumValue,PrismaClientInitializationError,PrismaClientKnownRequestError,PrismaClientRustPanicError,PrismaClientUnknownRequestError,PrismaClientValidationError,Public,Sql,Types,defineDmmfProperty,empty,getPrismaClient,itxClientDenyList,join,makeStrictEnum,objectEnumNames,objectEnumValues,raw,sqltag,warnEnvConflicts,warnOnce}); + /*! Bundled license information: + + decimal.js/decimal.mjs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71978c09301c61..072a2c91008376 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,24 +177,25 @@ Do not commit your `yarn.lock` unless you've made changes to the `package.json`. If your last commit has the `yarn.lock` file alongside other files and you only wish to uncommit the `yarn.lock`: - ```bash - git checkout HEAD~1 yarn.lock - git commit -m "Revert yarn.lock changes" - ``` +```bash +git checkout HEAD~1 yarn.lock +git commit -m "Revert yarn.lock changes" +``` _NB_: You may have to bypass the pre-commit hook with by appending `--no-verify` to the git commit If you've pushed the commit with the `yarn.lock`: - 1. Correct the commit locally using the above method. - 2. Carefully force push: +1. Correct the commit locally using the above method. +2. Carefully force push: - ```bash - git push origin --force - ``` +```bash +git push origin --force +``` If `yarn.lock` was committed a while ago and there have been several commits since, you can use the following steps to revert just the `yarn.lock` changes without impacting the subsequent changes: 1. **Checkout a Previous Version**: + - Find the commit hash before the `yarn.lock` was unintentionally committed. You can do this by viewing the Git log: ```bash @@ -208,6 +209,7 @@ If `yarn.lock` was committed a while ago and there have been several commits sin ``` 2. **Commit the Reverted Version**: + - After checking out the previous version of the `yarn.lock`, commit this change: ```bash @@ -215,6 +217,7 @@ If `yarn.lock` was committed a while ago and there have been several commits sin ``` 3. **Proceed with Caution**: + - If you need to push this change, first pull the latest changes from your remote branch to ensure you're not overwriting other recent changes: ```bash diff --git a/README.md b/README.md index dee240506a4e42..3ea2b22c626ab5 100644 --- a/README.md +++ b/README.md @@ -173,12 +173,30 @@ yarn dx #### Development tip -> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **tRPC**. +Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\ +Where {level} can be one of the following: + +`0` for silly \ +`1` for trace \ +`2` for debug \ +`3` for info \ +`4` for warn \ +`5` for error \ +`6` for fatal + +When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works: + +The logger will include all logs that are at the specified level or higher. For example: \ + +- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \ +- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \ ```sh -echo 'NEXT_PUBLIC_DEBUG=1' >> .env +echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env ``` +for Logger level to be set at info, for example. + #### Gitpod Setup 1. Click the button below to open this project in Gitpod. @@ -399,25 +417,25 @@ Cal.com, Inc. is a commercial open source company, which means some parts of thi > [!NOTE] > Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All commercial "Multiplayer APIs" are under a commercial license. -| | AGPLv3 | EE | -|---|---|---| -| Self-host for commercial purposes | ✅ | ✅ | -| Clone privately | ✅ | ✅ | -| Fork publicly | ✅ | ✅ | -| Requires CLA | ✅ | ✅ | -| Official Support| ❌ | ✅ | -| Derivative work privately | ❌ | ✅ | -| SSO | ❌ | ✅ | -| Admin Panel | ❌ | ✅ | -| Impersonation | ❌ | ✅ | -| Managed Event Types | ❌ | ✅ | -| Organizations | ❌ | ✅ | -| Payments | ❌ | ✅ | -| Platform | ❌ | ✅ | -| Teams | ❌ | ✅ | -| Users | ❌ | ✅ | -| Video | ❌ | ✅ | -| Workflows | ❌ | ✅ | +| | AGPLv3 | EE | +| --------------------------------- | ------ | --- | +| Self-host for commercial purposes | ✅ | ✅ | +| Clone privately | ✅ | ✅ | +| Fork publicly | ✅ | ✅ | +| Requires CLA | ✅ | ✅ | +|  Official Support | ❌  | ✅ | +| Derivative work privately | ❌ | ✅ | +|  SSO | ❌ | ✅ | +| Admin Panel | ❌ | ✅ | +| Impersonation | ❌ | ✅ | +| Managed Event Types | ❌ | ✅ | +| Organizations | ❌ | ✅ | +| Payments | ❌ | ✅ | +| Platform | ❌ | ✅ | +| Teams | ❌ | ✅ | +| Users | ❌ | ✅ | +| Video | ❌ | ✅ | +| Workflows | ❌ | ✅ | > [!TIP] > We work closely with the community and always invite feedback about what should be open and what is fine to be commercial. This list is not set and stone and we have moved things from commercial to open in the past. Please open a [discussion](https://github.com/calcom/cal.com/discussions) if you feel like something is wrong. @@ -513,17 +531,18 @@ following 1. Open [Zoom Marketplace](https://marketplace.zoom.us/) and sign in with your Zoom account. 2. On the upper right, click "Develop" => "Build App". -3. On "OAuth", select "Create". +3. Select "General App" , click "Create". 4. Name your App. -5. Choose "User-managed app" as the app type. -6. De-select the option to publish the app on the Zoom App Marketplace. -7. Click "Create". -8. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields. -9. Set the Redirect URL for OAuth `/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs. -10. Also add the redirect URL given above as an allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form. -11. You don't need to provide basic information about your app. Instead click on "Scopes" and then on "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`. -12. Click "Done". -13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings. +5. Choose "User-managed app" for "Select how the app is managed". +6. De-select the option to publish the app on the Zoom App Marketplace, if asked. +7. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields. +8. Set the "OAuth Redirect URL" under "OAuth Information" as `/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs. +9. Also add the redirect URL given above as an allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form. +10. You don't need to provide basic information about your app. Instead click on "Scopes" and then on "+ Add Scopes". On the left, + 1. click the category "Meeting" and check the scope `meeting:write:meeting`. + 2. click the category "User" and check the scope `user:read:settings`. +11. Click "Done". +12. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings. ### Obtaining Daily API Credentials diff --git a/apps/api/v1/lib/validations/booking.ts b/apps/api/v1/lib/validations/booking.ts index f09e80542313d0..33b61dacb34bb8 100644 --- a/apps/api/v1/lib/validations/booking.ts +++ b/apps/api/v1/lib/validations/booking.ts @@ -1,6 +1,13 @@ import { z } from "zod"; -import { _AttendeeModel, _BookingModel as Booking, _PaymentModel, _UserModel } from "@calcom/prisma/zod"; +import { + _AttendeeModel, + _BookingModel as Booking, + _EventTypeModel, + _PaymentModel, + _TeamModel, + _UserModel, +} from "@calcom/prisma/zod"; import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils"; import { schemaQueryUserId } from "./shared/queryUserId"; @@ -46,7 +53,23 @@ export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams .merge(schemaBookingEditParams) .omit({ uid: true }); +const teamSchema = _TeamModel.pick({ + name: true, + slug: true, +}); + export const schemaBookingReadPublic = Booking.extend({ + eventType: _EventTypeModel + .pick({ + title: true, + slug: true, + }) + .merge( + z.object({ + team: teamSchema.nullish(), + }) + ) + .nullish(), attendees: z .array( _AttendeeModel.pick({ @@ -87,6 +110,7 @@ export const schemaBookingReadPublic = Booking.extend({ timeZone: true, attendees: true, user: true, + eventType: true, payment: true, metadata: true, status: true, diff --git a/apps/api/v1/lib/validations/shared/queryExpandRelations.ts b/apps/api/v1/lib/validations/shared/queryExpandRelations.ts new file mode 100644 index 00000000000000..f6a5115deb9fcf --- /dev/null +++ b/apps/api/v1/lib/validations/shared/queryExpandRelations.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +const expandEnum = z.enum(["team"]); + +export const schemaQuerySingleOrMultipleExpand = z + .union([ + expandEnum, // Allow a single value from the enum + z.array(expandEnum).refine((arr) => new Set(arr).size === arr.length, { + message: "Array values must be unique", + }), // Allow an array of enum values, with uniqueness constraint + ]) + .optional(); diff --git a/apps/api/v1/pages/api/bookings/[id]/_get.ts b/apps/api/v1/pages/api/bookings/[id]/_get.ts index bc8511b5867bb1..01eafee532e0cf 100644 --- a/apps/api/v1/pages/api/bookings/[id]/_get.ts +++ b/apps/api/v1/pages/api/bookings/[id]/_get.ts @@ -4,6 +4,7 @@ import { defaultResponder } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; import { schemaBookingReadPublic } from "~/lib/validations/booking"; +import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; /** @@ -89,9 +90,21 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform export async function getHandler(req: NextApiRequest) { const { query } = req; const { id } = schemaQueryIdParseInt.parse(query); + + const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand); + const expand = Array.isArray(queryFilterForExpand) + ? queryFilterForExpand + : queryFilterForExpand + ? [queryFilterForExpand] + : []; const booking = await prisma.booking.findUnique({ where: { id }, - include: { attendees: true, user: true, payment: true }, + include: { + attendees: true, + user: true, + payment: true, + eventType: expand.includes("team") ? { include: { team: true } } : false, + }, }); return { booking: schemaBookingReadPublic.parse(booking) }; } diff --git a/apps/api/v1/pages/api/bookings/_get.ts b/apps/api/v1/pages/api/bookings/_get.ts index 2174841e97d721..e9ec08b4b72a4c 100644 --- a/apps/api/v1/pages/api/bookings/_get.ts +++ b/apps/api/v1/pages/api/bookings/_get.ts @@ -12,6 +12,7 @@ import { } from "~/lib/utils/retrieveScopedAccessibleUsers"; import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking"; import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail"; +import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations"; import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId"; /** @@ -216,10 +217,18 @@ export async function handler(req: NextApiRequest) { args.take = take; args.skip = skip; } + const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand); + const expand = Array.isArray(queryFilterForExpand) + ? queryFilterForExpand + : queryFilterForExpand + ? [queryFilterForExpand] + : []; + args.include = { attendees: true, user: true, payment: true, + eventType: expand.includes("team") ? { include: { team: true } } : false, }; const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query); diff --git a/apps/api/v1/test/lib/bookings/_get.integration-test.ts b/apps/api/v1/test/lib/bookings/_get.integration-test.ts index 74206dd83b3679..4d915a634a7ef3 100644 --- a/apps/api/v1/test/lib/bookings/_get.integration-test.ts +++ b/apps/api/v1/test/lib/bookings/_get.integration-test.ts @@ -164,7 +164,6 @@ describe("GET /api/bookings", async () => { const responseData = await handler(req); responseData.bookings.forEach((booking) => { - console.log(booking); expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); }); }); @@ -188,4 +187,27 @@ describe("GET /api/bookings", async () => { }); }); }); + + describe("Expand feature to add relational data in return payload", () => { + it("Returns only team data when expand=team is set", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const { req } = createMocks({ + method: "GET", + query: { + expand: "team", + }, + pagination: DefaultPagination, + }); + + req.userId = adminUser.id; + req.isOrganizationOwnerOrAdmin = true; + + const responseData = await handler(req); + console.log("bookings=>", responseData.bookings); + responseData.bookings.forEach((booking) => { + if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1"); + if (booking.id === 19) expect(booking.eventType?.team).toBe(null); + }); + }); + }); }); diff --git a/apps/api/v2/README.md b/apps/api/v2/README.md index 9de22f30c4ca5a..e67f17751e6859 100644 --- a/apps/api/v2/README.md +++ b/apps/api/v2/README.md @@ -44,8 +44,8 @@ Copy `.env.example` to `.env` and fill values. ## Add license Key to deployments table in DB -id, logo theme licenseKey agreedLicenseAt -1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611' +id, logo theme licenseKey agreedLicenseAt +1, null, null, 'c4234812-12ab-42s6-a1e3-55bedd4a5bb7', '2023-05-15 21:39:47.611' your CALCOM_LICENSE_KEY env var need to contain the same value diff --git a/apps/api/v2/jest-e2e.json b/apps/api/v2/jest-e2e.json index a0c04f1046cf49..fe8f6ba838c8db 100644 --- a/apps/api/v2/jest-e2e.json +++ b/apps/api/v2/jest-e2e.json @@ -11,5 +11,7 @@ "^.+\\.(t|j)s$": "ts-jest" }, "setupFiles": ["/test/setEnvVars.ts"], - "reporters": ["default", "jest-summarizing-reporter"] + "reporters": ["default", "jest-summarizing-reporter"], + "workerIdleMemoryLimit": "512MB", + "maxWorkers": 2 } diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 5eda351e7920cc..c2cad589ac0f96 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn dev:build && jest --watch", "test:cov": "yarn dev:build && jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json", + "test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192' jest --ci --forceExit --config ./jest-e2e.json", "test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch", "prisma": "yarn workspace @calcom/prisma prisma", "generate-schemas": "yarn prisma generate && yarn prisma format", @@ -28,7 +28,7 @@ "dependencies": { "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.34", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.37", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/app.module.ts b/apps/api/v2/src/app.module.ts index 76531dd09c01a7..16f55de93add34 100644 --- a/apps/api/v2/src/app.module.ts +++ b/apps/api/v2/src/app.module.ts @@ -1,4 +1,5 @@ import appConfig from "@/config/app"; +import { CustomThrottlerGuard } from "@/lib/throttler-guard"; import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware"; import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware"; import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware"; @@ -15,7 +16,7 @@ import { BullModule } from "@nestjs/bull"; import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core"; -import { seconds, ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; +import { seconds, ThrottlerModule } from "@nestjs/throttler"; import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis"; import { AppController } from "./app.controller"; @@ -32,6 +33,7 @@ import { AppController } from "./app.controller"; BullModule.forRoot({ redis: `${process.env.REDIS_URL}${process.env.NODE_ENV === "production" ? "?tls=true" : ""}`, }), + // Rate limiting here is handled by the CustomThrottlerGuard ThrottlerModule.forRootAsync({ imports: [RedisModule], inject: [RedisService], @@ -59,7 +61,7 @@ import { AppController } from "./app.controller"; }, { provide: APP_GUARD, - useClass: ThrottlerGuard, + useClass: CustomThrottlerGuard, }, ], }) diff --git a/apps/api/v2/src/ee/bookings/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts similarity index 80% rename from apps/api/v2/src/ee/bookings/bookings.module.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts index b91c65392ec13c..de0b63ddf377cb 100644 --- a/apps/api/v2/src/ee/bookings/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts @@ -1,4 +1,4 @@ -import { BookingsController } from "@/ee/bookings/controllers/bookings.controller"; +import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller"; import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; import { BillingModule } from "@/modules/billing/billing.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; @@ -12,6 +12,6 @@ import { Module } from "@nestjs/common"; @Module({ imports: [PrismaModule, RedisModule, TokensModule, BillingModule], providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeyRepository], - controllers: [BookingsController], + controllers: [BookingsController_2024_04_15], }) -export class BookingsModule {} +export class BookingsModule_2024_04_15 {} diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts similarity index 95% rename from apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts index 698c869e9f08da..0e249c11fdb636 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts @@ -1,8 +1,8 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; -import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input"; -import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output"; -import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; +import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; +import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; @@ -24,7 +24,7 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { handleNewBooking } from "@calcom/platform-libraries"; import { ApiSuccessResponse, ApiResponse, ApiErrorResponse } from "@calcom/platform-types"; -describe("Bookings Endpoints", () => { +describe("Bookings Endpoints 2024-04-15", () => { describe("User Authenticated", () => { let app: INestApplication; @@ -111,7 +111,7 @@ describe("Bookings Endpoints", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { start: bookingStart, end: bookingEnd, eventTypeId: bookingEventTypeId, @@ -166,7 +166,7 @@ describe("Bookings Endpoints", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { start: bookingStart, end: bookingEnd, eventTypeId: bookingEventTypeId, @@ -209,7 +209,7 @@ describe("Bookings Endpoints", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { start: bookingStart, end: bookingEnd, eventTypeId: bookingEventTypeId, @@ -266,7 +266,7 @@ describe("Bookings Endpoints", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { rescheduleUid: createdBooking.uid, start: newBookingStart, end: newBookingEnd, @@ -305,7 +305,7 @@ describe("Bookings Endpoints", () => { return request(app.getHttpServer()) .get("/v2/bookings?filters[status]=upcoming") .then((response) => { - const responseBody: GetBookingsOutput = response.body; + const responseBody: GetBookingsOutput_2024_04_15 = response.body; expect(responseBody.data.bookings.length).toEqual(2); const fetchedBooking = responseBody.data.bookings.find( @@ -329,7 +329,7 @@ describe("Bookings Endpoints", () => { return request(app.getHttpServer()) .get(`/v2/bookings/${createdBooking.uid}`) .then((response) => { - const responseBody: GetBookingOutput = response.body; + const responseBody: GetBookingOutput_2024_04_15 = response.body; const bookingInfo = responseBody.data; expect(responseBody.status).toEqual(SUCCESS_STATUS); diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts similarity index 87% rename from apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index a23bf5dd8c8a1d..069b1a52bfe52e 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -1,11 +1,11 @@ -import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input"; -import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input"; -import { MarkNoShowInput } from "@/ee/bookings/inputs/mark-no-show.input"; -import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output"; -import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output"; -import { MarkNoShowOutput } from "@/ee/bookings/outputs/mark-no-show.output"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; +import { CreateRecurringBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-recurring-booking.input"; +import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark-no-show.input"; +import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; +import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; +import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output"; import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions"; import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; @@ -31,7 +31,7 @@ import { UseGuards, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; import { NextApiRequest } from "next/types"; @@ -40,10 +40,10 @@ import { v4 as uuidv4 } from "uuid"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants"; import { + handleNewRecurringBooking, handleNewBooking, BookingResponse, HttpError, - handleNewRecurringBooking, handleInstantMeeting, handleMarkNoShow, getAllUserBookings, @@ -52,7 +52,11 @@ import { getBookingForReschedule, ErrorCode, } from "@calcom/platform-libraries"; -import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types"; +import { + GetBookingsInput_2024_04_15, + CancelBookingInput_2024_04_15, + Status_2024_04_15, +} from "@calcom/platform-types"; import { ApiResponse } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; @@ -80,11 +84,11 @@ const DEFAULT_PLATFORM_PARAMS = { @Controller({ path: "/v2/bookings", - version: API_VERSIONS_VALUES, + version: [VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14], }) @UseGuards(PermissionsGuard) -@DocsTags("Bookings") -export class BookingsController { +@DocsExcludeController(true) +export class BookingsController_2024_04_15 { private readonly logger = new Logger("BookingsController"); constructor( @@ -99,16 +103,16 @@ export class BookingsController { @Get("/") @UseGuards(ApiAuthGuard) @Permissions([BOOKING_READ]) - @ApiQuery({ name: "filters[status]", enum: Status, required: true }) + @ApiQuery({ name: "filters[status]", enum: Status_2024_04_15, required: true }) @ApiQuery({ name: "limit", type: "number", required: false }) @ApiQuery({ name: "cursor", type: "number", required: false }) async getBookings( @GetUser() user: User, - @Query() queryParams: GetBookingsInput - ): Promise { + @Query() queryParams: GetBookingsInput_2024_04_15 + ): Promise { const { filters, cursor, limit } = queryParams; const bookings = await getAllUserBookings({ - bookingListingByStatus: filters.status, + bookingListingByStatus: [filters.status], skip: cursor ?? 0, take: limit ?? 10, filters, @@ -125,7 +129,7 @@ export class BookingsController { } @Get("/:bookingUid") - async getBooking(@Param("bookingUid") bookingUid: string): Promise { + async getBooking(@Param("bookingUid") bookingUid: string): Promise { const { bookingInfo } = await getBookingInfo(bookingUid); if (!bookingInfo) { @@ -155,7 +159,7 @@ export class BookingsController { @Post("/") async createBooking( @Req() req: BookingRequest, - @Body() body: CreateBookingInput, + @Body() body: CreateBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise>> { const oAuthClientId = clientId?.toString(); @@ -186,7 +190,7 @@ export class BookingsController { async cancelBooking( @Req() req: BookingRequest, @Param("bookingId") bookingId: string, - @Body() _: CancelBookingInput, + @Body() _: CancelBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise> { const oAuthClientId = clientId?.toString(); @@ -219,9 +223,9 @@ export class BookingsController { @UseGuards(ApiAuthGuard) async markNoShow( @GetUser("id") userId: number, - @Body() body: MarkNoShowInput, + @Body() body: MarkNoShowInput_2024_04_15, @Param("bookingUid") bookingUid: string - ): Promise { + ): Promise { try { const markNoShowResponse = await handleMarkNoShow({ bookingUid: bookingUid, @@ -240,7 +244,7 @@ export class BookingsController { @Post("/recurring") async createRecurringBooking( @Req() req: BookingRequest, - @Body() _: CreateRecurringBookingInput[], + @Body() _: CreateRecurringBookingInput_2024_04_15[], @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise> { const oAuthClientId = clientId?.toString(); @@ -278,7 +282,7 @@ export class BookingsController { @Post("/instant") async createInstantBooking( @Req() req: BookingRequest, - @Body() _: CreateBookingInput, + @Body() _: CreateBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise>>> { const oAuthClientId = clientId?.toString(); @@ -369,7 +373,12 @@ export class BookingsController { const oAuthParams = oAuthClientId ? await this.getOAuthClientsParams(oAuthClientId) : DEFAULT_PLATFORM_PARAMS; - Object.assign(req, { userId, ...oAuthParams, platformBookingLocation }); + Object.assign(req, { + userId, + ...oAuthParams, + platformBookingLocation, + noEmail: !oAuthParams.arePlatformEmailsEnabled, + }); return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams; } diff --git a/apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts similarity index 97% rename from apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts index 0feeb0cc19a77b..ed0275acf06152 100644 --- a/apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts @@ -40,7 +40,7 @@ class Response { notes?: string; } -export class CreateBookingInput { +export class CreateBookingInput_2024_04_15 { @IsString() @IsOptional() end?: string; diff --git a/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts similarity index 72% rename from apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts index 658e12aaa2293a..7201519bf7f0d2 100644 --- a/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts @@ -1,9 +1,9 @@ -import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator"; import type { AppsStatus } from "@calcom/platform-libraries"; -export class CreateRecurringBookingInput extends CreateBookingInput { +export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 { @IsBoolean() @IsOptional() noEmail?: boolean; diff --git a/apps/api/v2/src/ee/bookings/inputs/mark-no-show.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/inputs/mark-no-show.input.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts index 0630f08fcc3395..017f9edd78c8ce 100644 --- a/apps/api/v2/src/ee/bookings/inputs/mark-no-show.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts @@ -9,7 +9,7 @@ class Attendee { noShow!: boolean; } -export class MarkNoShowInput { +export class MarkNoShowInput_2024_04_15 { @IsBoolean() @IsOptional() noShowHost?: boolean; diff --git a/apps/api/v2/src/ee/bookings/outputs/get-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts similarity index 92% rename from apps/api/v2/src/ee/bookings/outputs/get-booking.output.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts index c770a583059624..07e352736e00d9 100644 --- a/apps/api/v2/src/ee/bookings/outputs/get-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts @@ -88,7 +88,7 @@ class EventType { timeZone!: string | null; } -class GetBookingData { +class GetBookingData_2024_04_15 { @IsString() title!: string; @@ -159,15 +159,15 @@ class GetBookingData { eventType!: EventType | null; } -export class GetBookingOutput { +export class GetBookingOutput_2024_04_15 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: GetBookingData, + type: GetBookingData_2024_04_15, }) @ValidateNested() - @Type(() => GetBookingData) - data!: GetBookingData; + @Type(() => GetBookingData_2024_04_15) + data!: GetBookingData_2024_04_15; } diff --git a/apps/api/v2/src/ee/bookings/outputs/get-bookings.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/outputs/get-bookings.output.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts index 09e555fbb135f5..7e27580bc70bd0 100644 --- a/apps/api/v2/src/ee/bookings/outputs/get-bookings.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts @@ -202,7 +202,7 @@ class GetBookingsDataEntry { rescheduled?: any; } -class GetBookingsData { +class GetBookingsData_2024_04_15 { @ValidateNested() @Type(() => GetBookingsDataEntry) @IsArray() @@ -215,15 +215,15 @@ class GetBookingsData { nextCursor!: number | null; } -export class GetBookingsOutput { +export class GetBookingsOutput_2024_04_15 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: GetBookingsData, + type: GetBookingsData_2024_04_15, }) @ValidateNested() - @Type(() => GetBookingsData) - data!: GetBookingsData; + @Type(() => GetBookingsData_2024_04_15) + data!: GetBookingsData_2024_04_15; } diff --git a/apps/api/v2/src/ee/bookings/outputs/mark-no-show.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts similarity index 78% rename from apps/api/v2/src/ee/bookings/outputs/mark-no-show.output.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts index a321848b139bf2..c951987c7c1a47 100644 --- a/apps/api/v2/src/ee/bookings/outputs/mark-no-show.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts @@ -12,7 +12,7 @@ class Attendee { noShow!: boolean; } -class HandleMarkNoShowData { +class HandleMarkNoShowData_2024_04_15 { @IsString() message!: string; @@ -27,15 +27,15 @@ class HandleMarkNoShowData { attendees?: Attendee[]; } -export class MarkNoShowOutput { +export class MarkNoShowOutput_2024_04_15 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: HandleMarkNoShowData, + type: HandleMarkNoShowData_2024_04_15, }) @ValidateNested() - @Type(() => HandleMarkNoShowData) - data!: HandleMarkNoShowData; + @Type(() => HandleMarkNoShowData_2024_04_15) + data!: HandleMarkNoShowData_2024_04_15; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts new file mode 100644 index 00000000000000..21737c4717fe9f --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -0,0 +1,33 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; +import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; +import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; +import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; +import { BillingModule } from "@/modules/billing/billing.module"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RedisModule } from "@/modules/redis/redis.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { TokensRepository } from "@/modules/tokens/tokens.repository"; +import { UsersModule } from "@/modules/users/users.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule], + providers: [ + TokensRepository, + OAuthFlowService, + OAuthClientRepository, + BookingsService_2024_08_13, + InputBookingsService_2024_08_13, + OutputBookingsService_2024_08_13, + BookingsRepository_2024_08_13, + EventTypesRepository_2024_06_14, + ApiKeyRepository, + ], + controllers: [BookingsController_2024_08_13], +}) +export class BookingsModule_2024_08_13 {} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts new file mode 100644 index 00000000000000..fea95d8b845628 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -0,0 +1,86 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class BookingsRepository_2024_08_13 { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async getById(id: number) { + return this.dbRead.prisma.booking.findUnique({ + where: { + id, + }, + }); + } + + async getByIdsWithAttendeesAndUser(ids: number[]) { + return this.dbRead.prisma.booking.findMany({ + where: { + id: { + in: ids, + }, + }, + include: { + attendees: true, + user: true, + }, + }); + } + + async getByUid(bookingUid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid: bookingUid, + }, + }); + } + + async getByIdWithAttendeesAndUser(id: number) { + return this.dbRead.prisma.booking.findUnique({ + where: { + id, + }, + include: { + attendees: true, + user: true, + }, + }); + } + + async getByUidWithAttendeesAndUser(uid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid, + }, + include: { + attendees: true, + user: true, + }, + }); + } + + async getRecurringByUidWithAttendeesAndUser(uid: string) { + return this.dbRead.prisma.booking.findMany({ + where: { + recurringEventId: uid, + }, + include: { + attendees: true, + user: true, + }, + }); + } + + async getByFromReschedule(fromReschedule: string) { + return this.dbRead.prisma.booking.findFirst({ + where: { + fromReschedule, + }, + include: { + attendees: true, + user: true, + }, + }); + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts new file mode 100644 index 00000000000000..f28af04e4e63eb --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts @@ -0,0 +1,160 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { Team } from "@calcom/prisma/client"; + +describe("Bookings Endpoints 2024-08-13", () => { + describe("With api key", () => { + let app: INestApplication; + let organization: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let teamRepositoryFixture: TeamRepositoryFixture; + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; + let apiKeyString: string; + + const userEmail = "bookings-controller-e2e@api.com"; + let user: User; + + let eventTypeId: number; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); + + organization = await teamRepositoryFixture.create({ name: "organization bookings" }); + + user = await userRepositoryFixture.create({ + email: userEmail, + }); + + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); + apiKeyString = keyString; + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(user.id, userSchedule); + const event = await eventTypesRepositoryFixture.create( + { title: "peer coding", slug: "peer-coding", length: 60 }, + user.id + ); + eventTypeId = event.id; + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + }); + + describe("create bookings", () => { + it("should create a booking with api key", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId, + attendee: { + name: "Mr Key", + email: "mr_key@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(eventTypeId); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + }); + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(user.email); + await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts new file mode 100644 index 00000000000000..d7f84c4a010154 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -0,0 +1,219 @@ +import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; +import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; +import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; +import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { + Controller, + Post, + Logger, + Body, + UseGuards, + Req, + Get, + Param, + Query, + HttpCode, + HttpStatus, +} from "@nestjs/common"; +import { + ApiOperation, + ApiTags as DocsTags, + ApiHeader, + getSchemaPath, + ApiBody, + ApiExtraModels, +} from "@nestjs/swagger"; +import { User } from "@prisma/client"; +import { Request } from "express"; + +import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { + CreateBookingInputPipe, + CreateBookingInput, + GetBookingsInput_2024_08_13, + RescheduleBookingInput_2024_08_13, + CancelBookingInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, + CreateBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, +} from "@calcom/platform-types"; + +@Controller({ + path: "/v2/bookings", + version: VERSION_2024_08_13_VALUE, +}) +@UseGuards(PermissionsGuard) +@DocsTags("Bookings") +@ApiHeader({ + name: "cal-api-version", + description: `Must be set to \`2024-08-13\``, + required: true, +}) +export class BookingsController_2024_08_13 { + private readonly logger = new Logger("BookingsController"); + + constructor(private readonly bookingsService: BookingsService_2024_08_13) {} + + @Post("/") + @ApiOperation({ + summary: "Create booking", + description: ` + POST /v2/bookings is used to create regular bookings, recurring bookings and instant bookings. The request bodies for all 3 are almost the same except: + If eventTypeId in the request body is id of a regular event, then regular booking is created. + + If it is an id of a recurring event type, then recurring booking is created. + + Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible. + + For team event types it is possible to create instant meeting. To do that just pass \`"instant": true\` to the request body. + + The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone. + `, + }) + @ApiBody({ + schema: { + oneOf: [ + { $ref: getSchemaPath(CreateBookingInput_2024_08_13) }, + { $ref: getSchemaPath(CreateInstantBookingInput_2024_08_13) }, + { $ref: getSchemaPath(CreateRecurringBookingInput_2024_08_13) }, + ], + }, + description: + "Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13", + }) + @ApiExtraModels( + CreateBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13 + ) + async createBooking( + @Body(new CreateBookingInputPipe()) + body: CreateBookingInput, + @Req() request: Request + ): Promise { + const booking = await this.bookingsService.createBooking(request, body); + + if (Array.isArray(booking)) { + await this.bookingsService.billBookings(booking); + } else { + await this.bookingsService.billBooking(booking); + } + + return { + status: SUCCESS_STATUS, + data: booking, + }; + } + + @Get("/:bookingUid") + @UseGuards(BookingUidGuard) + @ApiOperation({ + summary: "Get booking", + description: `\`:bookingUid\` can be + + 1. uid of a normal booking + + 2. uid of one of the recurring booking recurrences + + 3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).`, + }) + async getBooking(@Param("bookingUid") bookingUid: string): Promise { + const booking = await this.bookingsService.getBooking(bookingUid); + + return { + status: SUCCESS_STATUS, + data: booking, + }; + } + + @Get("/") + @UseGuards(ApiAuthGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) + @Permissions([BOOKING_READ]) + async getBookings( + @Query() queryParams: GetBookingsInput_2024_08_13, + @GetUser() user: User + ): Promise { + const bookings = await this.bookingsService.getBookings(queryParams, user); + + return { + status: SUCCESS_STATUS, + data: bookings, + }; + } + + @Post("/:bookingUid/reschedule") + @UseGuards(BookingUidGuard) + @ApiOperation({ + summary: "Reschedule booking", + description: + "Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.", + }) + async rescheduleBooking( + @Param("bookingUid") bookingUid: string, + @Body() body: RescheduleBookingInput_2024_08_13, + @Req() request: Request + ): Promise { + const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body); + await this.bookingsService.billRescheduledBooking(newBooking, bookingUid); + + return { + status: SUCCESS_STATUS, + data: newBooking, + }; + } + + @Post("/:bookingUid/cancel") + @UseGuards(BookingUidGuard) + @HttpCode(HttpStatus.OK) + async cancelBooking( + @Req() request: Request, + @Param("bookingUid") bookingUid: string, + @Body() body: CancelBookingInput_2024_08_13 + ): Promise { + const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body); + + return { + status: SUCCESS_STATUS, + data: cancelledBooking, + }; + } + + @Post("/:bookingUid/mark-absent") + @HttpCode(HttpStatus.OK) + @Permissions([BOOKING_WRITE]) + @UseGuards(ApiAuthGuard, BookingUidGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) + async markNoShow( + @Param("bookingUid") bookingUid: string, + @Body() body: MarkAbsentBookingInput_2024_08_13, + @GetUser("id") ownerId: number + ): Promise { + const booking = await this.bookingsService.markAbsent(bookingUid, ownerId, body); + + return { + status: SUCCESS_STATUS, + data: booking, + }; + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts new file mode 100644 index 00000000000000..1f3139e60c4a50 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts @@ -0,0 +1,435 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { + CreateBookingInput_2024_08_13, + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, +} from "@calcom/platform-types"; +import { PlatformOAuthClient, Team } from "@calcom/prisma/client"; + +describe("Bookings Endpoints 2024-08-13", () => { + describe("Team bookings", () => { + let app: INestApplication; + let organization: Team; + let team1: Team; + let team2: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let oAuthClient: PlatformOAuthClient; + let teamRepositoryFixture: TeamRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + let hostsRepositoryFixture: HostsRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + + const teamUserEmail = "orgUser1team1@api.com"; + const teamUserEmail2 = "orgUser2team1@api.com"; + let teamUser: User; + let teamUser2: User; + + let team1EventTypeId: number; + let team2EventTypeId: number; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + teamUserEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + + organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" }); + team1 = await teamRepositoryFixture.create({ + name: "team 1", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + team2 = await teamRepositoryFixture.create({ + name: "team 2", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + oAuthClient = await createOAuthClient(organization.id); + + teamUser = await userRepositoryFixture.create({ + email: teamUserEmail, + locale: "it", + name: "orgUser1team1", + }); + + teamUser2 = await userRepositoryFixture.create({ + email: teamUserEmail2, + locale: "es", + name: "orgUser2team1", + }); + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(teamUser.id, userSchedule); + await schedulesService.createUserSchedule(teamUser2.id, userSchedule); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser.id}`, + username: teamUserEmail, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser.id, + }, + }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser2.id}`, + username: teamUserEmail2, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser2.id, + }, + }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser.id } }, + team: { connect: { id: team1.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser.id } }, + team: { connect: { id: team2.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser2.id } }, + team: { connect: { id: team2.id } }, + accepted: true, + }); + + const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: team1.id }, + }, + title: "Collective Event Type", + slug: "collective-event-type", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + team1EventTypeId = team1EventType.id; + + const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: team2.id }, + }, + title: "Collective Event Type 2", + slug: "collective-event-type-2", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + team2EventTypeId = team2EventType.id; + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser.id, + }, + }, + eventType: { + connect: { + id: team1EventType.id, + }, + }, + }); + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser.id, + }, + }, + eventType: { + connect: { + id: team2EventType.id, + }, + }, + }); + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser2.id, + }, + }, + eventType: { + connect: { + id: team2EventType.id, + }, + }, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + describe("create team bookings", () => { + it("should create a team 1 booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId: team1EventTypeId, + attendee: { + name: "alice", + email: "alice@gmail.com", + timeZone: "Europe/Madrid", + language: "es", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts.length).toEqual(1); + expect(data.hosts[0].id).toEqual(teamUser.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(team1EventTypeId); + expect(data.attendees.length).toEqual(1); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should create a team 2 booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), + eventTypeId: team2EventTypeId, + attendee: { + name: "bob", + email: "bob@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts.length).toEqual(1); + expect(data.hosts[0].id).toEqual(teamUser.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(team2EventTypeId); + expect(data.attendees.length).toEqual(2); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.attendees[1]).toEqual({ + name: teamUser2.name, + timeZone: teamUser2.timeZone, + language: teamUser2.locale, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + }); + + describe("get team bookings", () => { + it("should should get bookings by teamId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamId=${team1.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + expect(data[0].eventTypeId).toEqual(team1EventTypeId); + }); + }); + + it("should should get bookings by teamId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamId=${team2.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + expect(data[0].eventTypeId).toEqual(team2EventTypeId); + }); + }); + + it("should should get bookings by teamIds", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamIds=${team1.id},${team2.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined(); + expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined(); + }); + }); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["http://localhost:5555"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(teamUser.email); + await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/user-bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/user-bookings.controller.e2e-spec.ts new file mode 100644 index 00000000000000..1203c8bf6a0a5e --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/user-bookings.controller.e2e-spec.ts @@ -0,0 +1,1034 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; +import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; +import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { + CAL_API_VERSION_HEADER, + SUCCESS_STATUS, + VERSION_2024_08_13, + X_CAL_CLIENT_ID, +} from "@calcom/platform-constants"; +import { + CreateBookingInput_2024_08_13, + BookingOutput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, + RecurringBookingOutput_2024_08_13, + RescheduleBookingInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, +} from "@calcom/platform-types"; +import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client"; + +describe("Bookings Endpoints 2024-08-13", () => { + describe("User bookings", () => { + let app: INestApplication; + let organization: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let oAuthClient: PlatformOAuthClient; + let teamRepositoryFixture: TeamRepositoryFixture; + + const userEmail = "bookings-controller-e2e@api.com"; + let user: User; + + let eventTypeId: number; + let recurringEventTypeId: number; + + let createdBooking: BookingOutput_2024_08_13; + let rescheduledBooking: BookingOutput_2024_08_13; + let createdRecurringBooking: RecurringBookingOutput_2024_08_13[]; + + let bookingInThePast: Booking; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + + organization = await teamRepositoryFixture.create({ name: "organization bookings" }); + oAuthClient = await createOAuthClient(organization.id); + + user = await userRepositoryFixture.create({ + email: userEmail, + }); + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(user.id, userSchedule); + const event = await eventTypesRepositoryFixture.create( + { title: "peer coding", slug: "peer-coding", length: 60 }, + user.id + ); + eventTypeId = event.id; + + const recurringEvent = await eventTypesRepositoryFixture.create( + // note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row + { + title: "peer coding recurring", + slug: "peer-coding-recurring", + length: 60, + recurringEvent: { freq: 2, count: 3, interval: 1 }, + }, + user.id + ); + recurringEventTypeId = recurringEvent.id; + + bookingInThePast = await bookingsRepositoryFixture.create({ + user: { + connect: { + id: user.id, + }, + }, + startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)), + endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)), + title: "peer coding lets goo", + uid: "booking-in-the-past", + eventType: { + connect: { + id: eventTypeId, + }, + }, + location: "integrations:daily", + customInputs: {}, + metadata: {}, + responses: { + name: "Oldie", + email: "oldie@gmail.com", + }, + attendees: { + create: { + email: "oldie@gmail.com", + name: "Oldie", + locale: "lv", + timeZone: "Europe/Rome", + }, + }, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["http://localhost:5555"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + }); + + describe("create bookings", () => { + it("should create a booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId, + attendee: { + name: "Mr Proper", + email: "mr_proper@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(eventTypeId); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + createdBooking = data; + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should create a recurring booking", async () => { + const body: CreateRecurringBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(), + eventTypeId: recurringEventTypeId, + attendee: { + name: "Mr Proper Recurring", + email: "mr_proper_recurring@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(3); + + const firstBooking = data[0]; + expect(firstBooking.id).toBeDefined(); + expect(firstBooking.uid).toBeDefined(); + expect(firstBooking.hosts[0].id).toEqual(user.id); + expect(firstBooking.status).toEqual("accepted"); + expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString()); + expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString()); + expect(firstBooking.duration).toEqual(60); + expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(firstBooking.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(firstBooking.meetingUrl).toEqual(body.meetingUrl); + expect(firstBooking.recurringBookingUid).toBeDefined(); + expect(firstBooking.absentHost).toEqual(false); + + const secondBooking = data[1]; + expect(secondBooking.id).toBeDefined(); + expect(secondBooking.uid).toBeDefined(); + expect(secondBooking.hosts[0].id).toEqual(user.id); + expect(secondBooking.status).toEqual("accepted"); + expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString()); + expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString()); + expect(secondBooking.duration).toEqual(60); + expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(secondBooking.recurringBookingUid).toBeDefined(); + expect(secondBooking.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(secondBooking.meetingUrl).toEqual(body.meetingUrl); + expect(secondBooking.absentHost).toEqual(false); + + const thirdBooking = data[2]; + expect(thirdBooking.id).toBeDefined(); + expect(thirdBooking.uid).toBeDefined(); + expect(thirdBooking.hosts[0].id).toEqual(user.id); + expect(thirdBooking.status).toEqual("accepted"); + expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString()); + expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString()); + expect(thirdBooking.duration).toEqual(60); + expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(thirdBooking.recurringBookingUid).toBeDefined(); + expect(thirdBooking.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl); + expect(thirdBooking.absentHost).toEqual(false); + + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + }); + + describe("get individual booking", () => { + it("should should get a booking", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings/${createdBooking.uid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toEqual(createdBooking.id); + expect(data.uid).toEqual(createdBooking.uid); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual(createdBooking.status); + expect(data.start).toEqual(createdBooking.start); + expect(data.end).toEqual(createdBooking.end); + expect(data.duration).toEqual(createdBooking.duration); + expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); + expect(data.attendees[0]).toEqual(createdBooking.attendees[0]); + expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); + expect(data.absentHost).toEqual(createdBooking.absentHost); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should should get 1 recurrence of a recurring booking", async () => { + const recurrenceUid = createdRecurringBooking[0].uid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurrenceUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurranceBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurranceBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toEqual(createdRecurringBooking[0].id); + expect(data.uid).toEqual(createdRecurringBooking[0].uid); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual(createdRecurringBooking[0].status); + expect(data.start).toEqual(createdRecurringBooking[0].start); + expect(data.end).toEqual(createdRecurringBooking[0].end); + expect(data.duration).toEqual(createdRecurringBooking[0].duration); + expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); + expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]); + expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should should get all recurrences of the recurring bookings", async () => { + const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurringBookingUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(3); + + const firstBooking = data[0]; + expect(firstBooking.id).toEqual(createdRecurringBooking[0].id); + expect(firstBooking.uid).toEqual(createdRecurringBooking[0].uid); + expect(firstBooking.hosts[0].id).toEqual(user.id); + expect(firstBooking.status).toEqual(createdRecurringBooking[0].status); + expect(firstBooking.start).toEqual(createdRecurringBooking[0].start); + expect(firstBooking.end).toEqual(createdRecurringBooking[0].end); + expect(firstBooking.duration).toEqual(createdRecurringBooking[0].duration); + expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(firstBooking.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]); + expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost); + + const secondBooking = data[1]; + expect(secondBooking.id).toEqual(createdRecurringBooking[1].id); + expect(secondBooking.uid).toEqual(createdRecurringBooking[1].uid); + expect(secondBooking.hosts[0].id).toEqual(user.id); + expect(secondBooking.status).toEqual(createdRecurringBooking[1].status); + expect(secondBooking.start).toEqual(createdRecurringBooking[1].start); + expect(secondBooking.end).toEqual(createdRecurringBooking[1].end); + expect(secondBooking.duration).toEqual(createdRecurringBooking[1].duration); + expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId); + expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(secondBooking.attendees[0]).toEqual(createdRecurringBooking[1].attendees[0]); + expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl); + expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost); + + const thirdBooking = data[2]; + expect(thirdBooking.id).toEqual(createdRecurringBooking[2].id); + expect(thirdBooking.uid).toEqual(createdRecurringBooking[2].uid); + expect(thirdBooking.hosts[0].id).toEqual(user.id); + expect(thirdBooking.status).toEqual(createdRecurringBooking[2].status); + expect(thirdBooking.start).toEqual(createdRecurringBooking[2].start); + expect(thirdBooking.end).toEqual(createdRecurringBooking[2].end); + expect(thirdBooking.duration).toEqual(createdRecurringBooking[2].duration); + expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId); + expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(thirdBooking.attendees[0]).toEqual(createdRecurringBooking[2].attendees[0]); + expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl); + expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost); + + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + }); + + describe("get bookings", () => { + it("should should get all bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(5); + }); + }); + + it("should should take bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?take=3`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should skip bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?skip=2`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should get upcoming bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=upcoming`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(4); + }); + }); + + it("should should get past bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=past`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + }); + }); + + it("should should get upcoming and past bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=upcoming,past`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(5); + }); + }); + + it("should should get recurring booking recurrences", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=recurring`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should get bookings by attendee email", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?attendeeEmail=mr_proper@gmail.com`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + }); + }); + + it("should should get bookings by attendee name", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?attendeeName=Mr Proper Recurring`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should get bookings by eventTypeId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + }); + }); + + it("should should get bookings by eventTypeIds", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeIds=${eventTypeId},${recurringEventTypeId}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(5); + }); + }); + + it("should should get bookings by after specified start time", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?afterStart=${createdRecurringBooking[1].start}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + }); + }); + + it("should should get bookings by before specified end time", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?afterStart=${createdRecurringBooking[0].start}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should sort bookings by start in descending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + console.log("asap data", JSON.stringify(data, null, 2)); + expect(data[0].start).toEqual(createdBooking.start); + expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); + }); + }); + + it("should should sort bookings by start in ascending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=asc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString()); + expect(data[1].start).toEqual(createdBooking.start); + }); + }); + + it("should should sort bookings by end in descending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=desc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(createdBooking.start); + expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); + }); + }); + + it("should should sort bookings by end in ascending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=asc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString()); + expect(data[1].start).toEqual(createdBooking.start); + }); + }); + + it("should should sort bookings by created in descending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=desc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(createdBooking.start); + expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); + }); + }); + + it("should should sort bookings by created in ascending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=asc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString()); + expect(data[1].start).toEqual(createdBooking.start); + }); + }); + }); + + describe("reschedule bookings", () => { + it("should should reschedule normal booking", async () => { + const body: RescheduleBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(), + reschedulingReason: "Flying to mars that day", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdBooking.uid}/reschedule`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.reschedulingReason).toEqual(body.reschedulingReason); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString()); + expect(data.rescheduledFromUid).toEqual(createdBooking.uid); + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual(createdBooking.status); + expect(data.duration).toEqual(createdBooking.duration); + expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); + expect(data.attendees[0]).toEqual(createdBooking.attendees[0]); + expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); + expect(data.absentHost).toEqual(createdBooking.absentHost); + + rescheduledBooking = data; + }); + }); + + it("should set rescheduled booking status to cancelled", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings/${createdBooking.uid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.status).toEqual("cancelled"); + + createdBooking = data; + }); + }); + + it("should reschedule recurrence of a recurring booking", async () => { + const body: RescheduleBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(), + reschedulingReason: "Flying to mars again", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual(createdRecurringBooking[0].status); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString()); + expect(data.duration).toEqual(createdRecurringBooking[0].duration); + expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); + expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]); + expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); + + const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid); + expect(oldBooking).toBeDefined(); + expect(oldBooking?.status).toEqual("CANCELLED"); + }); + }); + + it("should get recurring booking recurrences after rescheduling one", async () => { + const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurringBookingUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(4); + const cancelled = data.find((booking) => booking.status === "cancelled"); + expect(cancelled).toBeDefined(); + const rescheduledNew = data.find( + (booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString() + ); + expect(rescheduledNew).toBeDefined(); + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + }); + + describe("mark absent", () => { + it("should mark host absent", async () => { + const body: MarkAbsentBookingInput_2024_08_13 = { + host: true, + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[1].uid}/mark-absent`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: MarkAbsentBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + const booking = createdRecurringBooking[1]; + expect(data.absentHost).toEqual(true); + + expect(data.id).toEqual(booking.id); + expect(data.uid).toEqual(booking.uid); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual(booking.status); + expect(data.start).toEqual(booking.start); + expect(data.end).toEqual(booking.end); + expect(data.duration).toEqual(booking.duration); + expect(data.eventTypeId).toEqual(booking.eventTypeId); + expect(data.attendees[0]).toEqual(booking.attendees[0]); + expect(data.meetingUrl).toEqual(booking.meetingUrl); + }); + }); + + it("should mark attendee absent", async () => { + const body: MarkAbsentBookingInput_2024_08_13 = { + attendees: [{ email: "mr_proper_recurring@gmail.com", absent: true }], + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[2].uid}/mark-absent`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: MarkAbsentBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + const booking = createdRecurringBooking[2]; + + expect(data.id).toEqual(booking.id); + expect(data.uid).toEqual(booking.uid); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual(booking.status); + expect(data.start).toEqual(booking.start); + expect(data.end).toEqual(booking.end); + expect(data.duration).toEqual(booking.duration); + expect(data.eventTypeId).toEqual(booking.eventTypeId); + expect(data.attendees[0].absent).toEqual(true); + expect(data.absentHost).toEqual(booking.absentHost); + expect(data.meetingUrl).toEqual(booking.meetingUrl); + }); + }); + }); + describe("cancel bookings", () => { + it("should cancel booking", async () => { + const body: CancelBookingInput_2024_08_13 = { + cancellationReason: "Going on a vacation", + }; + + const booking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid); + expect(booking).toBeDefined(); + expect(booking?.status).toEqual("ACCEPTED"); + + return request(app.getHttpServer()) + .post(`/v2/bookings/${rescheduledBooking.uid}/cancel`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set(X_CAL_CLIENT_ID, oAuthClient.id) + .expect(200) + .then(async (response) => { + const responseBody: CancelBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual("cancelled"); + expect(data.cancellationReason).toEqual(body.cancellationReason); + expect(data.start).toEqual(rescheduledBooking.start); + expect(data.end).toEqual(rescheduledBooking.end); + expect(data.duration).toEqual(rescheduledBooking.duration); + expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId); + expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]); + expect(data.meetingUrl).toEqual(rescheduledBooking.meetingUrl); + expect(data.absentHost).toEqual(rescheduledBooking.absentHost); + + const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid); + expect(cancelledBooking).toBeDefined(); + expect(cancelledBooking?.status).toEqual("CANCELLED"); + }); + }); + + it("should cancel recurring booking", async () => { + const body: CancelBookingInput_2024_08_13 = { + cancellationReason: "Going on a vacation", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[1].recurringBookingUid}/cancel`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set(X_CAL_CLIENT_ID, oAuthClient.id) + .expect(200) + .then(async (response) => { + const responseBody: CancelBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(4); + + const firstBooking = data[0]; + expect(firstBooking.status).toEqual("cancelled"); + + const secondBooking = data[1]; + expect(secondBooking.status).toEqual("cancelled"); + + const thirdBooking = data[2]; + expect(thirdBooking.status).toEqual("cancelled"); + + const fourthBooking = data[3]; + expect(fourthBooking.status).toEqual("cancelled"); + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + }); + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 { + return ( + !Array.isArray(data) && + typeof data === "object" && + data && + "id" in data && + "recurringBookingUid" in data + ); + } + + function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] { + return Array.isArray(data); + } + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(user.email); + await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts b/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts new file mode 100644 index 00000000000000..05d508a4fb38e9 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts @@ -0,0 +1,16 @@ +import { Injectable, CanActivate, ExecutionContext, BadRequestException } from "@nestjs/common"; + +@Injectable() +export class BookingUidGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + + const bookingUid = request.params.bookingUid; + + if (!bookingUid) { + throw new BadRequestException("Booking UID missing in the request path"); + } + + return true; + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts new file mode 100644 index 00000000000000..f463f698611f18 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts @@ -0,0 +1,26 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class CancelBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + ], + description: + "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects", + }) + @Type(() => Object) + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts new file mode 100644 index 00000000000000..deab5e452ac518 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -0,0 +1,25 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class CreateBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + ], + description: + "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects", + }) + @Type(() => Object) + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts new file mode 100644 index 00000000000000..aa4745785e7c2d --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts @@ -0,0 +1,26 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class GetBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + ], + description: + "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects", + }) + @Type(() => Object) + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts new file mode 100644 index 00000000000000..3f3c0623a2e392 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts @@ -0,0 +1,26 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class GetBookingsOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: "array", + items: { + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + ], + }, + description: + "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects", + }) + @ValidateNested({ each: true }) + data!: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts new file mode 100644 index 00000000000000..16dcaae6a1a7e4 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts @@ -0,0 +1,25 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class MarkAbsentBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + ], + description: + "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object", + }) + @ValidateNested() + @Type(() => Object) + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts new file mode 100644 index 00000000000000..2c45b9dd3ca5e7 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts @@ -0,0 +1,25 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class RescheduleBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + ], + description: + "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object", + }) + @ValidateNested() + @Type(() => Object) + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts new file mode 100644 index 00000000000000..e9956fcbe419f2 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -0,0 +1,274 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; +import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { BillingService } from "@/modules/billing/services/billing.service"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; +import { BadRequestException } from "@nestjs/common"; +import { Request } from "express"; + +import { + handleNewBooking, + handleNewRecurringBooking, + getAllUserBookings, + handleInstantMeeting, + handleCancelBooking, + handleMarkNoShow, +} from "@calcom/platform-libraries"; +import { + CreateBookingInput_2024_08_13, + RescheduleBookingInput_2024_08_13, + CreateBookingInput, + CreateRecurringBookingInput_2024_08_13, + GetBookingsInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, + CancelBookingInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, +} from "@calcom/platform-types"; +import { PrismaClient } from "@calcom/prisma"; + +type CreatedBooking = { + hosts: { id: number }[]; + uid: string; + start: string; +}; + +@Injectable() +export class BookingsService_2024_08_13 { + private readonly logger = new Logger("BookingsService"); + constructor( + private readonly inputService: InputBookingsService_2024_08_13, + private readonly outputService: OutputBookingsService_2024_08_13, + private readonly bookingsRepository: BookingsRepository_2024_08_13, + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly prismaReadService: PrismaReadService, + private readonly billingService: BillingService + ) {} + + async createBooking(request: Request, body: CreateBookingInput) { + try { + if ("instant" in body && body.instant) { + return await this.createInstantBooking(request, body); + } + + if (await this.isRecurring(body)) { + return await this.createRecurringBooking(request, body); + } + + return await this.createRegularBooking(request, body); + } catch (error) { + if (error instanceof Error) { + if (error.message === "no_available_users_found_error") { + throw new BadRequestException("User either already has booking at this time or is not available"); + } + } + throw error; + } + } + + async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createBookingRequest(request, body); + const booking = await handleInstantMeeting(bookingRequest); + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.bookingId); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.bookingId} was not found in the database`); + } + + return this.outputService.getOutputBooking(databaseBooking); + } + + async isRecurring(body: CreateBookingInput) { + const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId); + return !!eventType?.recurringEvent; + } + + async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); + const bookings = await handleNewRecurringBooking(bookingRequest); + const uid = bookings[0].recurringEventId; + if (!uid) { + throw new Error("Recurring booking was not created"); + } + + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid); + return this.outputService.getOutputRecurringBookings(recurringBooking); + } + + async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createBookingRequest(request, body); + const booking = await handleNewBooking(bookingRequest); + + if (!booking.id) { + throw new Error("Booking was not created"); + } + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + return this.outputService.getOutputBooking(databaseBooking); + } + + async getBooking(uid: string) { + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(uid); + + if (booking) { + const isRecurring = !!booking.recurringEventId; + if (isRecurring) { + return this.outputService.getOutputRecurringBooking(booking); + } + return this.outputService.getOutputBooking(booking); + } + + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid); + if (!recurringBooking.length) { + throw new NotFoundException(`Booking with uid=${uid} was not found in the database`); + } + + return this.outputService.getOutputRecurringBookings(recurringBooking); + } + + async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { + const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({ + bookingListingByStatus: queryParams.status || [], + skip: queryParams.skip ?? 0, + // note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason + take: queryParams.take ? queryParams.take - 1 : 100, + filters: this.inputService.transformGetBookingsFilters(queryParams), + ctx: { + user, + prisma: this.prismaReadService.prisma as unknown as PrismaClient, + }, + sort: this.inputService.transformGetBookingsSort(queryParams), + }); + // note(Lauris): fetchedBookings don't have attendees information and responses and i don't want to add them to the handler query, + // because its used elsewhere in code that does not need that information, so i get ids, fetch bookings and then return them formatted in same order as ids. + const ids = fetchedBookings.bookings.map((booking) => booking.id); + const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUser(ids); + + const bookingMap = new Map(bookings.map((booking) => [booking.id, booking])); + const orderedBookings = ids.map((id) => bookingMap.get(id)); + + const formattedBookings: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = []; + for (const booking of orderedBookings) { + if (!booking) { + continue; + } + + const formatted = { + ...booking, + eventTypeId: booking.eventTypeId, + startTime: new Date(booking.startTime), + endTime: new Date(booking.endTime), + absentHost: !!booking.noShowHost, + }; + + const isRecurring = !!formatted.recurringEventId; + if (isRecurring) { + formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted)); + } else { + formattedBookings.push(this.outputService.getOutputBooking(formatted)); + } + } + + return formattedBookings; + } + + async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput_2024_08_13) { + try { + const bookingRequest = await this.inputService.createRescheduleBookingRequest( + request, + bookingUid, + body + ); + const booking = await handleNewBooking(bookingRequest); + if (!booking.id) { + throw new Error("Booking was not created"); + } + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + if (databaseBooking.recurringEventId) { + return this.outputService.getOutputRecurringBooking(databaseBooking); + } + return this.outputService.getOutputBooking(databaseBooking); + } catch (error) { + if (error instanceof Error) { + if (error.message === "no_available_users_found_error") { + throw new BadRequestException("User either already has booking at this time or is not available"); + } + } + throw error; + } + } + + async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body); + await handleCancelBooking(bookingRequest); + return this.getBooking(bookingUid); + } + + async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) { + const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body); + + await handleMarkNoShow({ + bookingUid, + attendees: bodyTransformed.attendees, + noShowHost: bodyTransformed.noShowHost, + userId: bookingOwnerId, + }); + + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid); + + if (!booking) { + throw new Error(`Booking with uid=${bookingUid} was not found in the database`); + } + + const isRecurring = !!booking.recurringEventId; + if (isRecurring) { + return this.outputService.getOutputRecurringBooking(booking); + } + return this.outputService.getOutputBooking(booking); + } + + async billBookings(bookings: CreatedBooking[]) { + for (const booking of bookings) { + await this.billBooking(booking); + } + } + + async billBooking(booking: CreatedBooking) { + const hostId = booking.hosts[0].id; + if (!hostId) { + this.logger.error(`Booking with uid=${booking.uid} has no host`); + return; + } + + await this.billingService.increaseUsageByUserId(hostId, { + uid: booking.uid, + startTime: new Date(booking.start), + }); + } + + async billRescheduledBooking(newBooking: CreatedBooking, oldBookingUid: string) { + const hostId = newBooking.hosts[0].id; + if (!hostId) { + this.logger.error(`Booking with uid=${newBooking.uid} has no host`); + return; + } + + await this.billingService.increaseUsageByUserId(hostId, { + uid: newBooking.uid, + startTime: new Date(newBooking.start), + fromReschedule: oldBookingUid, + }); + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts new file mode 100644 index 00000000000000..a74123242bec98 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -0,0 +1,418 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { bookingResponsesSchema } from "@/ee/bookings/2024-08-13/services/output.service"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; +import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Request } from "express"; +import { DateTime } from "luxon"; +import { NextApiRequest } from "next/types"; +import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; + +import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; +import { + CancelBookingInput_2024_08_13, + CreateBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, + GetBookingsInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, + RescheduleBookingInput_2024_08_13, +} from "@calcom/platform-types"; + +type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams; + +const DEFAULT_PLATFORM_PARAMS = { + platformClientId: "", + platformCancelUrl: "", + platformRescheduleUrl: "", + platformBookingUrl: "", + arePlatformEmailsEnabled: false, + platformBookingLocation: undefined, +}; + +type OAuthRequestParams = { + platformClientId: string; + platformRescheduleUrl: string; + platformCancelUrl: string; + platformBookingUrl: string; + platformBookingLocation?: string; + arePlatformEmailsEnabled: boolean; +}; + +export enum Frequency { + "YEARLY", + "MONTHLY", + "WEEKLY", + "DAILY", + "HOURLY", + "MINUTELY", + "SECONDLY", +} + +const recurringEventSchema = z.object({ + dtstart: z.string().optional(), + interval: z.number().int().optional(), + count: z.number().int().optional(), + freq: z.nativeEnum(Frequency).optional(), + until: z.string().optional(), +}); + +@Injectable() +export class InputBookingsService_2024_08_13 { + private readonly logger = new Logger("InputBookingsService_2024_08_13"); + + constructor( + private readonly oAuthFlowService: OAuthFlowService, + private readonly oAuthClientRepository: OAuthClientRepository, + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly bookingsRepository: BookingsRepository_2024_08_13, + private readonly config: ConfigService, + private readonly apiKeyRepository: ApiKeyRepository + ) {} + + async createBookingRequest( + request: Request, + body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13 + ): Promise { + const bodyTransformed = await this.transformInputCreateBooking(body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + + const location = request.body.meetingUrl; + Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); + + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + return newRequest as unknown as BookingRequest; + } + + async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + inputBooking.eventTypeId + ); + + if (!eventType) { + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); + } + + const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( + inputBooking.attendee.timeZone + ); + const endTime = startTime.plus({ minutes: eventType.length }); + + return { + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: inputBooking.eventTypeId, + timeZone: inputBooking.attendee.timeZone, + language: inputBooking.attendee.language || "en", + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // metadata: inputBooking.metadata || {}, + metadata: {}, + hasHashedBookingLink: false, + guests: inputBooking.guests, + // note(Lauris): responses with name and email are required by the handleNewBooking + responses: inputBooking.bookingFieldsResponses + ? { + ...inputBooking.bookingFieldsResponses, + name: inputBooking.attendee.name, + email: inputBooking.attendee.email, + } + : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, + }; + } + + async createRecurringBookingRequest( + request: Request, + body: CreateRecurringBookingInput_2024_08_13 + ): Promise { + // note(Lauris): update to this.transformInputCreate when rescheduling is implemented + const bodyTransformed = await this.transformInputCreateRecurringBooking(body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + + const location = request.body.meetingUrl; + Object.assign(newRequest, { + userId, + ...oAuthParams, + platformBookingLocation: location, + noEmail: !oAuthParams.arePlatformEmailsEnabled, + }); + + newRequest.body = bodyTransformed.map((event) => ({ + ...event, + })); + + return newRequest as unknown as BookingRequest; + } + + async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) { + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + inputBooking.eventTypeId + ); + if (!eventType) { + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); + } + if (!eventType.recurringEvent) { + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`); + } + + const occurrence = recurringEventSchema.parse(eventType.recurringEvent); + const repeatsEvery = occurrence.interval; + const repeatsTimes = occurrence.count; + // note(Lauris): timeBetween 0=yearly, 1=monthly and 2=weekly + const timeBetween = occurrence.freq; + + if (!repeatsTimes) { + throw new Error("Repeats times is required"); + } + + const events = []; + const recurringEventId = uuidv4(); + + let startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( + inputBooking.attendee.timeZone + ); + + for (let i = 0; i < repeatsTimes; i++) { + const endTime = startTime.plus({ minutes: eventType.length }); + + events.push({ + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: inputBooking.eventTypeId, + recurringEventId, + timeZone: inputBooking.attendee.timeZone, + language: inputBooking.attendee.language || "en", + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // metadata: inputBooking.metadata || {}, + metadata: {}, + hasHashedBookingLink: false, + guests: inputBooking.guests, + // note(Lauris): responses with name and email are required by the handleNewBooking + responses: inputBooking.bookingFieldsResponses + ? { + ...inputBooking.bookingFieldsResponses, + name: inputBooking.attendee.name, + email: inputBooking.attendee.email, + } + : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, + schedulingType: eventType.schedulingType, + }); + + switch (timeBetween) { + case 0: // Yearly + startTime = startTime.plus({ years: repeatsEvery }); + break; + case 1: // Monthly + startTime = startTime.plus({ months: repeatsEvery }); + break; + case 2: // Weekly + startTime = startTime.plus({ weeks: repeatsEvery }); + break; + default: + throw new Error("Unsupported timeBetween value"); + } + } + + return events; + } + + async createRescheduleBookingRequest( + request: Request, + bookingUid: string, + body: RescheduleBookingInput_2024_08_13 + ): Promise { + const bodyTransformed = await this.transformInputRescheduleBooking(bookingUid, body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + + const location = await this.getRescheduleBookingLocation(bookingUid); + Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); + + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + return newRequest as unknown as BookingRequest; + } + + async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) { + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid); + if (!booking) { + throw new NotFoundException(`Booking with uid=${bookingUid} not found`); + } + if (!booking.eventTypeId) { + throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`); + } + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); + if (!eventType) { + throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); + } + + const bookingResponses = bookingResponsesSchema.parse(booking.responses); + const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email); + + if (!attendee) { + throw new NotFoundException( + `Attendee with e-mail ${bookingResponses.email} for booking with uid=${bookingUid} not found` + ); + } + + const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone); + const endTime = startTime.plus({ minutes: eventType.length }); + + return { + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: eventType.id, + timeZone: attendee.timeZone, + language: attendee.locale, + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // metadata: booking.metadata || {}, + metadata: {}, + hasHashedBookingLink: false, + guests: bookingResponses.guests, + responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason }, + rescheduleUid: bookingUid, + }; + } + + async getRescheduleBookingLocation(rescheduleBookingUid: string) { + const booking = await this.bookingsRepository.getByUid(rescheduleBookingUid); + if (!booking) { + throw new NotFoundException(`Booking with uid=${rescheduleBookingUid} not found`); + } + return booking.location; + } + + private async createBookingRequestOwnerId(req: Request): Promise { + try { + const bearerToken = req.get("Authorization")?.replace("Bearer ", ""); + if (bearerToken) { + if (isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_")) { + const strippedApiKey = stripApiKey(bearerToken, this.config.get("api.keyPrefix")); + const apiKeyHash = hashAPIKey(strippedApiKey); + const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash); + return keyData?.userId; + } else { + // Access Token + return this.oAuthFlowService.getOwnerId(bearerToken); + } + } + } catch (err) { + this.logger.error(err); + } + } + + private async createBookingRequestOAuthClientParams(clientId: string) { + const params = DEFAULT_PLATFORM_PARAMS; + try { + const client = await this.oAuthClientRepository.getOAuthClient(clientId); + if (client) { + params.platformClientId = clientId; + params.platformCancelUrl = client.bookingCancelRedirectUri ?? ""; + params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? ""; + params.platformBookingUrl = client.bookingRedirectUri ?? ""; + params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false; + } + return params; + } catch (err) { + this.logger.error(err); + return params; + } + } + + transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) { + return { + attendeeEmail: queryParams.attendeeEmail, + attendeeName: queryParams.attendeeName, + afterStartDate: queryParams.afterStart, + beforeEndDate: queryParams.beforeEnd, + teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), + eventTypeIds: + queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), + }; + } + + transformGetBookingsSort(queryParams: GetBookingsInput_2024_08_13) { + if (!queryParams.sortStart && !queryParams.sortEnd && !queryParams.sortCreated) { + return undefined; + } + + return { + sortStart: queryParams.sortStart, + sortEnd: queryParams.sortEnd, + sortCreated: queryParams.sortCreated, + }; + } + + async createCancelBookingRequest( + request: Request, + bookingUid: string, + body: CancelBookingInput_2024_08_13 + ): Promise { + const bodyTransformed = await this.transformInputCancelBooking(bookingUid, body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + + Object.assign(newRequest, { userId, ...oAuthParams }); + + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + return newRequest as unknown as BookingRequest; + } + + async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) { + let allRemainingBookings = false; + let uid = bookingUid; + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(bookingUid); + + if (recurringBooking.length) { + // note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event + allRemainingBookings = true; + // note(Lauirs): we need to set uid as one of the individual recurring ids, not the main recurring event id + uid = recurringBooking[0].uid; + } + + return { + uid, + cancellationReason: inputBooking.cancellationReason, + allRemainingBookings, + }; + } + + transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) { + return { + noShowHost: inputBooking.host, + attendees: inputBooking.attendees?.map((attendee) => ({ + email: attendee.email, + noShow: attendee.absent, + })), + }; + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts new file mode 100644 index 00000000000000..27a2318fd365e8 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -0,0 +1,114 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { Injectable } from "@nestjs/common"; +import { plainToClass } from "class-transformer"; +import { DateTime } from "luxon"; +import { z } from "zod"; + +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { Booking } from "@calcom/prisma/client"; + +export const bookingResponsesSchema = z.object({ + email: z.string(), + name: z.string(), + guests: z.array(z.string()).optional(), + rescheduledReason: z.string().optional(), +}); + +type DatabaseBooking = Booking & { + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; +} & { user: { id: number; name: string | null; email: string } | null }; + +@Injectable() +export class OutputBookingsService_2024_08_13 { + constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {} + + getOutputBooking(databaseBooking: DatabaseBooking) { + const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); + const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); + const duration = dateEnd.diff(dateStart, "minutes").minutes; + + const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); + + const booking = { + id: databaseBooking.id, + uid: databaseBooking.uid, + hosts: [databaseBooking.user], + status: databaseBooking.status.toLowerCase(), + cancellationReason: databaseBooking.cancellationReason || undefined, + reschedulingReason: bookingResponses?.rescheduledReason, + rescheduledFromUid: databaseBooking.fromReschedule || undefined, + start: databaseBooking.startTime, + end: databaseBooking.endTime, + duration, + eventTypeId: databaseBooking.eventTypeId, + attendees: databaseBooking.attendees.map((attendee) => ({ + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: attendee.locale, + absent: !!attendee.noShow, + })), + guests: bookingResponses.guests, + meetingUrl: databaseBooking.location, + absentHost: !!databaseBooking.noShowHost, + }; + + return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); + } + + async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) { + const transformed = []; + + for (const booking of databaseBookings) { + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + transformed.push(this.getOutputRecurringBooking(databaseBooking)); + } + + return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()); + } + + getOutputRecurringBooking(databaseBooking: DatabaseBooking) { + const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); + const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); + const duration = dateEnd.diff(dateStart, "minutes").minutes; + + const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); + + const booking = { + id: databaseBooking.id, + uid: databaseBooking.uid, + hosts: [databaseBooking.user], + status: databaseBooking.status.toLowerCase(), + cancellationReason: databaseBooking.cancellationReason || undefined, + reschedulingReason: bookingResponses?.rescheduledReason, + rescheduledFromUid: databaseBooking.fromReschedule || undefined, + start: databaseBooking.startTime, + end: databaseBooking.endTime, + duration, + eventTypeId: databaseBooking.eventTypeId, + attendees: databaseBooking.attendees.map((attendee) => ({ + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: attendee.locale, + absent: !!attendee.noShow, + })), + guests: bookingResponses.guests, + meetingUrl: databaseBooking.location, + recurringBookingUid: databaseBooking.recurringEventId, + absentHost: !!databaseBooking.noShowHost, + }; + + return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); + } +} diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts index 9b5ba327c0ea7c..e08562a65f9c96 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts @@ -36,7 +36,7 @@ import { InternalServerErrorException, ParseIntPipe, } from "@nestjs/common"; -import { ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2"; @@ -47,7 +47,7 @@ import { PrismaClient } from "@calcom/prisma"; version: [VERSION_2024_04_15, VERSION_2024_06_11], }) @UseGuards(PermissionsGuard) -@DocsTags("Event types") +@DocsExcludeController(true) export class EventTypesController_2024_04_15 { constructor( private readonly eventTypesService: EventTypesService_2024_04_15, diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts index 549851f6998334..2c6ac159fc4267 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts @@ -15,6 +15,7 @@ import { updateEventType, EventTypesPublic, getEventTypesPublic, + systemBeforeFieldEmail, } from "@calcom/platform-libraries"; import { EventType } from "@calcom/prisma/client"; @@ -127,8 +128,17 @@ export class EventTypesService_2024_04_15 { async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) { this.checkCanUpdateEventType(user.id, eventTypeId); const eventTypeUser = await this.getUserToUpdateEvent(user); + const bookingFields = [...(body.bookingFields || [])]; + + if ( + !bookingFields.find((field) => field.type === "email") && + !bookingFields.find((field) => field.type === "phone") + ) { + bookingFields.push(systemBeforeFieldEmail); + } + await updateEventType({ - input: { id: eventTypeId, ...body }, + input: { id: eventTypeId, ...body, bookingFields }, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts index 79bce6fc488ce1..3b8990c8b15de3 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -250,7 +250,6 @@ describe("Event types Endpoints", () => { expect(createdEventType.description).toEqual(body.description); expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes); expect(createdEventType.locations).toEqual(body.locations); - expect(createdEventType.bookingFields).toEqual(body.bookingFields); expect(createdEventType.ownerId).toEqual(user.id); expect(createdEventType.scheduleId).toEqual(firstSchedule.id); expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount); @@ -259,6 +258,16 @@ describe("Event types Endpoints", () => { expect(createdEventType.offsetStart).toEqual(body.offsetStart); expect(createdEventType.bookingWindow).toEqual(body.bookingWindow); expect(createdEventType.recurrence).toEqual(body.recurrence); + + const responseBookingFields = body.bookingFields || []; + const expectedBookingFields = [ + { isDefault: true, required: true, slug: "name", type: "name" }, + { isDefault: true, required: true, slug: "email", type: "email" }, + { isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" }, + ...responseBookingFields.map((field) => ({ isDefault: false, ...field })), + ]; + + expect(createdEventType.bookingFields).toEqual(expectedBookingFields); eventType = responseBody.data; }); }); @@ -476,6 +485,16 @@ describe("Event types Endpoints", () => { let legacyEventTypeId1: number; let legacyEventTypeId2: number; + const expectedReturnSystemFields = [ + { isDefault: true, required: true, slug: "name", type: "name" }, + { isDefault: true, required: true, slug: "email", type: "email" }, + { isDefault: true, type: "radioInput", slug: "location", required: false }, + { isDefault: true, required: true, slug: "title", type: "text" }, + { isDefault: true, required: false, slug: "notes", type: "textarea" }, + { isDefault: true, required: false, slug: "guests", type: "multiemail" }, + { isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" }, + ]; + beforeAll(async () => { const moduleRef = await withApiAuth( userEmail, @@ -545,7 +564,7 @@ describe("Event types Endpoints", () => { .expect(400); }); - it("should return empty bookingFields if system fields are the only one in database", async () => { + it("should return system bookingFields stored in database", async () => { const legacyEventTypeInput = { title: "legacy event type", description: "legacy event type description", @@ -638,11 +657,11 @@ describe("Event types Endpoints", () => { .then(async (response) => { const responseBody: ApiSuccessResponse = response.body; const fetchedEventType = responseBody.data; - expect(fetchedEventType.bookingFields).toEqual([]); + expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields); }); }); - it("should return user created bookingFields among system fields in the database", async () => { + it("should return user created bookingFields with system fields", async () => { const userDefinedBookingField = { name: "team", type: "textarea", @@ -755,7 +774,9 @@ describe("Event types Endpoints", () => { const fetchedEventType = responseBody.data; expect(fetchedEventType.bookingFields).toEqual([ + ...expectedReturnSystemFields, { + isDefault: false, type: userDefinedBookingField.type, slug: userDefinedBookingField.name, label: userDefinedBookingField.label, diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts index 01c048c6f03df7..f6f69ff52a1dcc 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts @@ -24,7 +24,7 @@ import { Delete, Query, } from "@nestjs/common"; -import { ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger"; import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; import { @@ -39,12 +39,23 @@ import { }) @UseGuards(PermissionsGuard) @DocsTags("Event types") +@ApiHeader({ + name: "cal-api-version", + description: `Must be set to \`2024-06-14\``, + required: true, +}) export class EventTypesController_2024_06_14 { constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {} @Post("/") @Permissions([EVENT_TYPE_WRITE]) @UseGuards(ApiAuthGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) async createEventType( @Body() body: CreateEventTypeInput_2024_06_14, @GetUser() user: UserWithProfile @@ -60,6 +71,12 @@ export class EventTypesController_2024_06_14 { @Get("/:eventTypeId") @Permissions([EVENT_TYPE_READ]) @UseGuards(ApiAuthGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) async getEventTypeById( @Param("eventTypeId") eventTypeId: string, @GetUser() user: UserWithProfile @@ -91,6 +108,12 @@ export class EventTypesController_2024_06_14 { @Patch("/:eventTypeId") @Permissions([EVENT_TYPE_WRITE]) @UseGuards(ApiAuthGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) @HttpCode(HttpStatus.OK) async updateEventType( @Param("eventTypeId") eventTypeId: number, @@ -108,6 +131,12 @@ export class EventTypesController_2024_06_14 { @Delete("/:eventTypeId") @Permissions([EVENT_TYPE_WRITE]) @UseGuards(ApiAuthGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) async deleteEventType( @Param("eventTypeId") eventTypeId: number, @GetUser("id") userId: number diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts index de55f2561088f4..1c0ce0f0c5896f 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -6,8 +6,8 @@ import { Injectable } from "@nestjs/common"; import { getEventTypeById, - transformApiEventTypeBookingFields, - transformApiEventTypeLocations, + transformBookingFieldsApiToInternal, + transformLocationsApiToInternal, } from "@calcom/platform-libraries"; import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; import type { PrismaClient } from "@calcom/prisma"; @@ -30,8 +30,8 @@ type InputEventTransformed = Omit< > & { length: number; slug: string; - locations?: ReturnType; - bookingFields?: ReturnType; + locations?: ReturnType; + bookingFields?: ReturnType; }; @Injectable() @@ -102,6 +102,13 @@ export class EventTypesRepository_2024_06_14 { }); } + async getEventTypeByIdWithOwnerAndTeam(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + include: { owner: true, team: true }, + }); + } + async getUserEventTypeBySlug(userId: number, slug: string) { return this.dbRead.prisma.eventType.findUnique({ where: { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 1901acd77a657e..b604b3632ab943 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -1,12 +1,16 @@ import { Injectable } from "@nestjs/common"; import { - transformApiEventTypeBookingFields, - transformApiEventTypeLocations, - transformApiEventTypeIntervalLimits, - transformApiEventTypeFutureBookingLimits, - transformApiEventTypeRecurrence, + transformBookingFieldsApiToInternal, + transformLocationsApiToInternal, + transformIntervalLimitsApiToInternal, + transformFutureBookingLimitsApiToInternal, + transformRecurrenceApiToInternal, + systemBeforeFieldName, + systemBeforeFieldEmail, + systemAfterFieldRescheduleReason, } from "@calcom/platform-libraries"; +import { systemBeforeFieldLocation } from "@calcom/platform-libraries"; import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; @Injectable() @@ -30,11 +34,12 @@ export class InputEventTypesService_2024_06_14 { ...rest } = inputEventType; + const hasMultipleLocations = (locations || defaultLocations).length > 1; const eventType = { ...rest, length: lengthInMinutes, locations: this.transformInputLocations(locations || defaultLocations), - bookingFields: this.transformInputBookingFields(bookingFields), + bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations), bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined, durationLimits: bookingLimitsDuration ? this.transformInputIntervalLimits(bookingLimitsDuration) @@ -59,11 +64,15 @@ export class InputEventTypesService_2024_06_14 { ...rest } = inputEventType; + const hasMultipleLocations = !!(locations && locations?.length > 1); + const eventType = { ...rest, length: lengthInMinutes, locations: locations ? this.transformInputLocations(locations) : undefined, - bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined, + bookingFields: bookingFields + ? this.transformInputBookingFields(bookingFields, hasMultipleLocations) + : undefined, schedule: scheduleId, bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined, durationLimits: bookingLimitsDuration @@ -77,23 +86,35 @@ export class InputEventTypesService_2024_06_14 { } transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) { - return transformApiEventTypeLocations(inputLocations); + return transformLocationsApiToInternal(inputLocations); } - transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) { - return transformApiEventTypeBookingFields(inputBookingFields); + transformInputBookingFields( + inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"], + hasMultipleLocations: boolean + ) { + const defaultFieldsBefore = [systemBeforeFieldName, systemBeforeFieldEmail]; + // note(Lauris): if event type has multiple locations then a radio button booking field has to be displayed to allow booker to pick location + if (hasMultipleLocations) { + defaultFieldsBefore.push(systemBeforeFieldLocation); + } + + const customFields = transformBookingFieldsApiToInternal(inputBookingFields); + const defaultFieldsAfter = [systemAfterFieldRescheduleReason]; + + return [...defaultFieldsBefore, ...customFields, ...defaultFieldsAfter]; } transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) { - return transformApiEventTypeIntervalLimits(inputBookingFields); + return transformIntervalLimitsApiToInternal(inputBookingFields); } transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) { - const res = transformApiEventTypeFutureBookingLimits(inputBookingWindow); + const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow); return !!res ? res : {}; } transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) { - return transformApiEventTypeRecurrence(recurrence); + return transformRecurrenceApiToInternal(recurrence); } } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts index 205793df44fe8e..82c7490a065b8e 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -4,17 +4,17 @@ import type { EventType, User, Schedule } from "@prisma/client"; import { EventTypeMetaDataSchema, userMetadata, - getResponseEventTypeLocations, - getResponseEventTypeBookingFields, + transformLocationsInternalToApi, + transformBookingFieldsInternalToApi, parseRecurringEvent, TransformedLocationsSchema, BookingFieldsSchema, SystemField, - UserField, + CustomField, parseBookingLimit, - getResponseEventTypeIntervalLimits, - getResponseEventTypeFutureBookingLimits, - getResponseEventTypeRecurrence, + transformIntervalLimitsInternalToApi, + transformFutureBookingLimitsInternalToApi, + transformRecurrenceInternalToApi, } from "@calcom/platform-libraries"; import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types"; @@ -144,19 +144,20 @@ export class OutputEventTypesService_2024_06_14 { transformLocations(locations: any) { if (!locations) return []; - return getResponseEventTypeLocations(TransformedLocationsSchema.parse(locations)); + return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations)); } - transformBookingFields(inputBookingFields: (SystemField | UserField)[] | null) { - if (!inputBookingFields) return []; - const userFields = inputBookingFields.filter((field) => field.editable === "user") as UserField[]; - return getResponseEventTypeBookingFields(userFields); + transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) { + if (!bookingFields) return []; + + return transformBookingFieldsInternalToApi(bookingFields); } transformRecurringEvent(recurringEvent: any) { if (!recurringEvent) return null; const recurringEventParsed = parseRecurringEvent(recurringEvent); - return getResponseEventTypeRecurrence(recurringEventParsed); + if (!recurringEventParsed) return null; + return transformRecurrenceInternalToApi(recurringEventParsed); } transformMetadata(metadata: any) { @@ -182,10 +183,10 @@ export class OutputEventTypesService_2024_06_14 { transformIntervalLimits(bookingLimits: any) { const bookingLimitsParsed = parseBookingLimit(bookingLimits); - return getResponseEventTypeIntervalLimits(bookingLimitsParsed); + return transformIntervalLimitsInternalToApi(bookingLimitsParsed); } transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) { - return getResponseEventTypeFutureBookingLimits(bookingLimits); + return transformFutureBookingLimitsInternalToApi(bookingLimits); } } diff --git a/apps/api/v2/src/ee/platform-endpoints-module.ts b/apps/api/v2/src/ee/platform-endpoints-module.ts index 0e7df6730a5f31..b02b611399c5cd 100644 --- a/apps/api/v2/src/ee/platform-endpoints-module.ts +++ b/apps/api/v2/src/ee/platform-endpoints-module.ts @@ -1,4 +1,5 @@ -import { BookingsModule } from "@/ee/bookings/bookings.module"; +import { BookingsModule_2024_04_15 } from "@/ee/bookings/2024-04-15/bookings.module"; +import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; import { CalendarsModule } from "@/ee/calendars/calendars.module"; import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; @@ -21,7 +22,8 @@ import { Module } from "@nestjs/common"; EventTypesModule_2024_04_15, EventTypesModule_2024_06_14, CalendarsModule, - BookingsModule, + BookingsModule_2024_04_15, + BookingsModule_2024_08_13, SlotsModule, ], }) diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts b/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts index ad6d31c06a0af6..54a6d7034b275b 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_04_15/controllers/schedules.controller.ts @@ -23,7 +23,7 @@ import { Patch, UseGuards, } from "@nestjs/common"; -import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiResponse, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; import { Throttle } from "@nestjs/throttler"; import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; @@ -36,7 +36,7 @@ import { CreateScheduleInput_2024_04_15 } from "../inputs/create-schedule.input" version: VERSION_2024_04_15_VALUE, }) @UseGuards(ApiAuthGuard, PermissionsGuard) -@DocsTags("Schedules") +@DocsExcludeController(true) export class SchedulesController_2024_04_15 { constructor(private readonly schedulesService: SchedulesService_2024_04_15) {} diff --git a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts index bb60283f078c8f..1c11d0fe825a9d 100644 --- a/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts +++ b/apps/api/v2/src/ee/schedules/schedules_2024_06_11/controllers/schedules.controller.ts @@ -17,7 +17,7 @@ import { Patch, UseGuards, } from "@nestjs/common"; -import { ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiHeader, ApiOperation, ApiResponse, ApiTags as DocsTags } from "@nestjs/swagger"; import { Throttle } from "@nestjs/throttler"; import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; @@ -38,11 +38,38 @@ import { }) @UseGuards(ApiAuthGuard, PermissionsGuard) @DocsTags("Schedules") +@ApiHeader({ + name: "cal-api-version", + description: `Must be set to \`2024-06-11\``, + required: true, +}) +@ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, +}) export class SchedulesController_2024_06_11 { constructor(private readonly schedulesService: SchedulesService_2024_06_11) {} @Post("/") @Permissions([SCHEDULE_WRITE]) + @ApiOperation({ + summary: "Create a schedule", + description: ` + The point of creating schedules is for event types to be available at specific times. + + First goal of schedules is to have a default schedule. If you are platform customer and created managed users, then it is important to note that each managed user should have a default schedule. + 1. If you passed \`timeZone\` when creating managed user, then the default schedule from Monday to Friday from 9AM to 5PM will be created with that timezone. Managed user can then change the default schedule via \`AvailabilitySettings\` atom. + 2. If you did not, then we assume you want that user has specific schedule right away. You should create default schedule by specifying + \`"isDefault": true\` in the request body. Until the user has a default schedule that user can't be booked or manage his / her schedule via the AvailabilitySettings atom. + + Second goal is to create other schedules that event types can point to, so that when that event is booked availability is not checked against the default schedule but against that specific schedule. + After creating a non default schedule you can update event type to point to that schedule via the PATCH \`event-types/{eventTypeId}\` endpoint. + + When specifying start time and end time for each day use 24 hour format e.g. 08:00, 15:00 etc. + `, + }) async createSchedule( @GetUser() user: UserWithProfile, @Body() bodySchedule: CreateScheduleInput_2024_06_11 @@ -59,7 +86,7 @@ export class SchedulesController_2024_06_11 { @Permissions([SCHEDULE_READ]) @ApiResponse({ status: 200, - description: "Returns the default schedule", + description: "Returns the default schedule of the authenticated user", type: GetDefaultScheduleOutput_2024_06_11, }) async getDefaultSchedule(@GetUser() user: UserWithProfile): Promise { @@ -88,6 +115,9 @@ export class SchedulesController_2024_06_11 { @Get("/") @Permissions([SCHEDULE_READ]) + @ApiOperation({ + description: "Returns all schedules of the authenticated user", + }) async getSchedules(@GetUser() user: UserWithProfile): Promise { const schedules = await this.schedulesService.getUserSchedules(user.id); diff --git a/apps/api/v2/src/lib/api-versions.ts b/apps/api/v2/src/lib/api-versions.ts index 62a70a4b83a372..ab9fcdae091c31 100644 --- a/apps/api/v2/src/lib/api-versions.ts +++ b/apps/api/v2/src/lib/api-versions.ts @@ -5,13 +5,16 @@ import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14, + VERSION_2024_08_13, } from "@calcom/platform-constants"; export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue; export const VERSION_2024_06_14_VALUE: VersionValue = VERSION_2024_06_14 as unknown as VersionValue; export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue; export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue; +export const VERSION_2024_08_13_VALUE: VersionValue = VERSION_2024_08_13 as unknown as VersionValue; export { VERSION_2024_04_15 }; export { VERSION_2024_06_11 }; export { VERSION_2024_06_14 }; +export { VERSION_2024_08_13 }; diff --git a/apps/api/v2/src/lib/throttler-guard.ts b/apps/api/v2/src/lib/throttler-guard.ts new file mode 100644 index 00000000000000..a54d3a0c850560 --- /dev/null +++ b/apps/api/v2/src/lib/throttler-guard.ts @@ -0,0 +1,45 @@ +import { isApiKey } from "@/lib/api-key"; +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Reflector } from "@nestjs/core"; +import { ThrottlerGuard, ThrottlerModuleOptions, ThrottlerStorage } from "@nestjs/throttler"; +import { Request } from "express"; + +import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; + +@Injectable() +export class CustomThrottlerGuard extends ThrottlerGuard { + private logger = new Logger("CustomThrottlerGuard"); + + constructor( + options: ThrottlerModuleOptions, + storageService: ThrottlerStorage, + reflector: Reflector, + private readonly config: ConfigService + ) { + super(options, storageService, reflector); + } + + protected async getTracker(request: Request): Promise { + const authorizationHeader = request.get("Authorization")?.replace("Bearer ", ""); + + if (authorizationHeader) { + return isApiKey(authorizationHeader, this.config.get("api.apiKeyPrefix") ?? "cal_") + ? `api_key_${authorizationHeader}` + : `access_token_${authorizationHeader}`; + } + + const oauthClientId = request.get(X_CAL_CLIENT_ID); + + if (oauthClientId) { + return oauthClientId; + } + + if (request.ip) { + return request.ip; + } + + this.logger.log(`no tracker found: ${request.url}`); + return "unknown"; + } +} diff --git a/apps/api/v2/src/modules/billing/billing.repository.ts b/apps/api/v2/src/modules/billing/billing.repository.ts index 54b8cf0b2a6129..656e07b61a0588 100644 --- a/apps/api/v2/src/modules/billing/billing.repository.ts +++ b/apps/api/v2/src/modules/billing/billing.repository.ts @@ -19,7 +19,7 @@ export class BillingRepository { billingStart: number, billingEnd: number, plan: PlatformPlan, - subscription?: string + subscriptionId?: string ) { return this.dbWrite.prisma.platformBilling.update({ where: { @@ -28,7 +28,7 @@ export class BillingRepository { data: { billingCycleStart: billingStart, billingCycleEnd: billingEnd, - subscriptionId: subscription, + subscriptionId, plan: plan.toString(), }, }); diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts index 3317d0d1e31d16..7e899b967d716f 100644 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts +++ b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts @@ -7,9 +7,8 @@ import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subsc import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto"; import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto"; import { BillingService } from "@/modules/billing/services/billing.service"; -import { PlatformPlan } from "@/modules/billing/types"; +import { StripeService } from "@/modules/stripe/stripe.service"; import { - BadRequestException, Body, Controller, Get, @@ -25,7 +24,6 @@ import { import { ConfigService } from "@nestjs/config"; import { ApiExcludeController } from "@nestjs/swagger"; import { Request } from "express"; -import { Stripe } from "stripe"; import { ApiResponse } from "@calcom/platform-types"; @@ -40,6 +38,7 @@ export class BillingController { constructor( private readonly billingService: BillingService, + public readonly stripeService: StripeService, private readonly configService: ConfigService ) { this.stripeWhSecret = configService.get("stripe.webhookSecret", { infer: true }) ?? ""; @@ -69,13 +68,32 @@ export class BillingController { @Param("teamId") teamId: number, @Body() input: SubscribeToPlanInput ): Promise> { - const { status } = await this.billingService.getBillingData(teamId); + const { action, url } = await this.billingService.createSubscriptionForTeam(teamId, input.plan); - if (status === "valid") { - throw new BadRequestException("This team is already subscribed to a plan."); + if (action === "redirect") { + return { + status: "success", + data: { + action: "redirect", + url, + }, + }; } - const { action, url } = await this.billingService.createSubscriptionForTeam(teamId, input.plan); + return { + status: "success", + }; + } + + @Post("/:teamId/upgrade") + @UseGuards(NextAuthGuard, OrganizationRolesGuard) + @MembershipRoles(["OWNER", "ADMIN"]) + async upgradeTeamBillingInStripe( + @Param("teamId") teamId: number, + @Body() input: SubscribeToPlanInput + ): Promise> { + const { action, url } = await this.billingService.updateSubscriptionForTeam(teamId, input.plan); + if (action === "redirect") { return { status: "success", @@ -103,33 +121,7 @@ export class BillingController { this.stripeWhSecret ); - if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") { - const subscription = event.data.object as Stripe.Subscription; - if (!subscription.metadata?.teamId) { - return { - status: "success", - }; - } - - const teamId = Number.parseInt(subscription.metadata.teamId); - const plan = subscription.metadata.plan; - if (!plan || !teamId) { - this.logger.log("Webhook received but not pertaining to Platform, discarding."); - return { - status: "success", - }; - } - - await this.billingService.setSubscriptionForTeam( - teamId, - subscription, - PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan] - ); - - return { - status: "success", - }; - } + await this.billingService.createOrUpdateStripeSubscription(event); return { status: "success", diff --git a/apps/api/v2/src/modules/billing/services/billing.service.ts b/apps/api/v2/src/modules/billing/services/billing.service.ts index 2cc24e2c7475b2..cb6bd4cbc63c6f 100644 --- a/apps/api/v2/src/modules/billing/services/billing.service.ts +++ b/apps/api/v2/src/modules/billing/services/billing.service.ts @@ -6,7 +6,13 @@ import { PlatformPlan } from "@/modules/billing/types"; import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; import { StripeService } from "@/modules/stripe/stripe.service"; import { InjectQueue } from "@nestjs/bull"; -import { Injectable, InternalServerErrorException, Logger, OnModuleDestroy } from "@nestjs/common"; +import { + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, + OnModuleDestroy, +} from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { Queue } from "bull"; import { DateTime } from "luxon"; @@ -43,12 +49,9 @@ export class BillingService implements OnModuleDestroy { async createSubscriptionForTeam(teamId: number, plan: PlatformPlan) { const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); - let brandNewBilling = false; - let customerId = teamWithBilling?.platformBilling?.customerId; if (!teamWithBilling?.platformBilling) { - brandNewBilling = true; customerId = await this.teamsRepository.createNewBillingRelation(teamId); this.logger.log("Team had no Stripe Customer ID, created one for them.", { @@ -57,43 +60,61 @@ export class BillingService implements OnModuleDestroy { }); } - if (brandNewBilling || !teamWithBilling?.platformBilling?.subscriptionId) { - const { url } = await this.stripeService.stripe.checkout.sessions.create({ - customer: customerId, - line_items: [ - { - price: this.billingConfigService.get(plan)?.overage, - }, - { - price: this.billingConfigService.get(plan)?.base, - quantity: 1, - }, - ], - success_url: `${this.webAppUrl}/settings/platform/`, - cancel_url: `${this.webAppUrl}/settings/platform/`, - mode: "subscription", + const { url } = await this.stripeService.stripe.checkout.sessions.create({ + customer: customerId, + line_items: [ + { + price: this.billingConfigService.get(plan)?.base, + quantity: 1, + }, + { + price: this.billingConfigService.get(plan)?.overage, + }, + ], + success_url: `${this.webAppUrl}/settings/platform/`, + cancel_url: `${this.webAppUrl}/settings/platform/`, + mode: "subscription", + metadata: { + teamId: teamId.toString(), + plan: plan.toString(), + }, + currency: "usd", + subscription_data: { metadata: { teamId: teamId.toString(), plan: plan.toString(), }, - subscription_data: { - metadata: { - teamId: teamId.toString(), - plan: plan.toString(), - }, - }, - allow_promotion_codes: true, - }); + }, + allow_promotion_codes: true, + }); - if (!url) throw new InternalServerErrorException("Failed to create Stripe session."); + if (!url) throw new InternalServerErrorException("Failed to create Stripe session."); - return { action: "redirect", url }; - } + return { action: "redirect", url }; + } + + async updateSubscriptionForTeam(teamId: number, plan: PlatformPlan) { + const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); + const customerId = teamWithBilling?.platformBilling?.customerId; + + const { url } = await this.stripeService.stripe.checkout.sessions.create({ + customer: customerId, + success_url: `${this.webAppUrl}/settings/platform/`, + cancel_url: `${this.webAppUrl}/settings/platform/plans`, + mode: "setup", + metadata: { + teamId: teamId.toString(), + plan: plan.toString(), + }, + currency: "usd", + }); + + if (!url) throw new InternalServerErrorException("Failed to create Stripe session."); - return { action: "none" }; + return { action: "redirect", url }; } - async setSubscriptionForTeam(teamId: number, subscription: Stripe.Subscription, plan: PlatformPlan) { + async setSubscriptionForTeam(teamId: number, subscriptionId: string, plan: PlatformPlan) { const billingCycleStart = DateTime.now().get("day"); const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day"); @@ -102,10 +123,94 @@ export class BillingService implements OnModuleDestroy { billingCycleStart, billingCycleEnd, plan, - subscription.id + subscriptionId ); } + async createOrUpdateStripeSubscription(event: Stripe.Event) { + if (event.type === "checkout.session.completed") { + const subscription = event.data.object as Stripe.Checkout.Session; + + if (!subscription.metadata?.teamId) { + return { + status: "success", + }; + } + + const teamId = Number.parseInt(subscription.metadata.teamId); + const plan = subscription.metadata.plan; + if (!plan || !teamId) { + this.logger.log("Webhook received but not pertaining to Platform, discarding."); + return { + status: "success", + }; + } + + if (subscription.mode === "subscription") { + await this.setSubscriptionForTeam( + teamId, + subscription.subscription as string, + PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan] + ); + } + + if (subscription.mode === "setup") { + await this.updateStripeSubscriptionForTeam(teamId, plan as PlatformPlan); + } + + return { + status: "success", + }; + } + } + + async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan) { + const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId); + + if (!teamWithBilling?.platformBilling || !teamWithBilling?.platformBilling.subscriptionId) { + throw new NotFoundException("Team plan not found"); + } + + const existingUserSubscription = await this.stripeService.stripe.subscriptions.retrieve( + teamWithBilling?.platformBilling?.subscriptionId + ); + const currentLicensedItem = existingUserSubscription.items.data.find( + (item) => item.price?.recurring?.usage_type === "licensed" + ); + const currentOverageItem = existingUserSubscription.items.data.find( + (item) => item.price?.recurring?.usage_type === "metered" + ); + + if (!currentLicensedItem) { + throw new NotFoundException("There is no licensed item present in the subscription"); + } + + if (!currentOverageItem) { + throw new NotFoundException("There is no overage item present in the subscription"); + } + + await this.stripeService.stripe.subscriptions.update(teamWithBilling?.platformBilling?.subscriptionId, { + items: [ + { + id: currentLicensedItem.id, + price: this.billingConfigService.get(plan)?.base, + }, + { + id: currentOverageItem.id, + price: this.billingConfigService.get(plan)?.overage, + clear_usage: false, + }, + ], + billing_cycle_anchor: "now", + proration_behavior: "create_prorations", + }); + + await this.setSubscriptionForTeam( + teamId, + teamWithBilling?.platformBilling?.subscriptionId, + PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan] + ); + } /** * * Adds a job to the queue to increment usage of a stripe subscription. diff --git a/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts b/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts index 80e4b7037dc6ef..aea7ef04d6110e 100644 --- a/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts @@ -98,6 +98,7 @@ describe("Platform Destination Calendar Endpoints", () => { error: { message: "" }, }, ], + destinationCalendar: null, }) ); app = moduleRef.createNestApplication(); diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts index 4665fa2896c699..033a4b86ead3c2 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts @@ -4,10 +4,9 @@ import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/con import { HttpExceptionFilter } from "@/filters/http-exception.filter"; import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; import { Locales } from "@/lib/enums/locales"; -import { - CreateUserResponse, - UserReturned, -} from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller"; +import { CreateManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output"; +import { GetManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-user.output"; +import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output"; import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input"; import { UsersModule } from "@/modules/users/users.module"; @@ -81,7 +80,7 @@ describe("OAuth Client Users Endpoints", () => { let schedulesRepositoryFixture: SchedulesRepositoryFixture; let profilesRepositoryFixture: ProfileRepositoryFixture; - let postResponseData: CreateUserResponse; + let postResponseData: CreateManagedUserOutput["data"]; const platformAdminEmail = "platform-sensei@mail.com"; let platformAdmin: User; @@ -150,6 +149,7 @@ describe("OAuth Client Users Endpoints", () => { const requestBody: CreateManagedUserInput = { email: userEmail, timeZone: "incorrect-time-zone", + name: "Alice Smith", }; await request(app.getHttpServer()) @@ -166,6 +166,7 @@ describe("OAuth Client Users Endpoints", () => { weekStart: "Monday", timeFormat: 24, locale: Locales.FR, + name: "Alice Smith", }; const response = await request(app.getHttpServer()) @@ -174,11 +175,7 @@ describe("OAuth Client Users Endpoints", () => { .send(requestBody) .expect(201); - const responseBody: ApiSuccessResponse<{ - user: Omit; - accessToken: string; - refreshToken: string; - }> = response.body; + const responseBody: CreateManagedUserOutput = response.body; postResponseData = responseBody.data; @@ -186,6 +183,7 @@ describe("OAuth Client Users Endpoints", () => { expect(responseBody.data).toBeDefined(); expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); expect(responseBody.data.user.timeZone).toEqual(requestBody.timeZone); + expect(responseBody.data.user.name).toEqual(requestBody.name); expect(responseBody.data.user.weekStart).toEqual(requestBody.weekStart); expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat); expect(responseBody.data.user.locale).toEqual(requestBody.locale); @@ -242,12 +240,13 @@ describe("OAuth Client Users Endpoints", () => { .set("Origin", `${CLIENT_REDIRECT_URI}`) .expect(200); - const responseBody: ApiSuccessResponse = response.body; + const responseBody: GetManagedUsersOutput = response.body; expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); expect(responseBody.data?.length).toBeGreaterThan(0); expect(responseBody.data[0].email).toEqual(postResponseData.user.email); + expect(responseBody.data[0].name).toEqual(postResponseData.user.name); }); it(`/GET/:id`, async () => { @@ -257,7 +256,7 @@ describe("OAuth Client Users Endpoints", () => { .set("Origin", `${CLIENT_REDIRECT_URI}`) .expect(200); - const responseBody: ApiSuccessResponse = response.body; + const responseBody: GetManagedUserOutput = response.body; expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); @@ -333,7 +332,7 @@ describe("OAuth Client Users Endpoints", () => { let eventTypesRepositoryFixture: EventTypesRepositoryFixture; let profileRepositoryFixture: ProfileRepositoryFixture; - let postResponseData: CreateUserResponse; + let postResponseData: CreateManagedUserOutput["data"]; const userEmail = "oauth-client-users-user@gmail.com"; const userTimeZone = "Europe/Rome"; @@ -479,6 +478,7 @@ describe("OAuth Client Users Endpoints", () => { weekStart: "Monday", timeFormat: 24, locale: Locales.FR, + name: "Alice Smith", }; const response = await request(app.getHttpServer()) @@ -487,12 +487,7 @@ describe("OAuth Client Users Endpoints", () => { .send(requestBody) .expect(201); - const responseBody: ApiSuccessResponse<{ - user: Omit; - accessToken: string; - refreshToken: string; - }> = response.body; - + const responseBody: CreateManagedUserOutput = response.body; postResponseData = responseBody.data; expect(responseBody.status).toEqual(SUCCESS_STATUS); @@ -500,6 +495,7 @@ describe("OAuth Client Users Endpoints", () => { await userHasCorrectEventTypes(responseBody.data.user.id); await teamHasCorrectEventTypes(team1.id); + expect(responseBody.data.user.name).toEqual(requestBody.name); }); async function userHasCorrectEventTypes(userId: number) { diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index b81124117b8a9b..58206f1fc443b9 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -79,7 +79,6 @@ export class OAuthClientUsersController { `Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}` ); const client = await this.oauthRepository.getOAuthClient(oAuthClientId); - console.log("asap createUser client", JSON.stringify(client, null, 2)); const isPlatformManaged = true; const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser( @@ -189,6 +188,7 @@ export class OAuthClientUsersController { id: user.id, email: user.email, username: user.username, + name: user.name, timeZone: user.timeZone, weekStart: user.weekStart, createdDate: user.createdDate, @@ -198,7 +198,3 @@ export class OAuthClientUsersController { }; } } - -export type UserReturned = Pick; - -export type CreateUserResponse = { user: UserReturned; accessToken: string; refreshToken: string }; diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output.ts index 5dc46fd4a43c28..fee340c659fdee 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output.ts @@ -13,6 +13,9 @@ export class ManagedUserOutput { @ApiProperty({ example: "alice" }) username!: string | null; + @ApiProperty({ example: "alice" }) + name!: string | null; + @ApiProperty({ example: "America/New_York" }) timeZone!: string; diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts index c3788b9a87e3bc..bc9d941288ac6d 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts @@ -172,6 +172,7 @@ export class OAuthClientsController { id: user.id, email: user.email, username: user.username, + name: user.name, timeZone: user.timeZone, weekStart: user.weekStart, createdDate: user.createdDate, diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index b2a421077f73ef..c8ca87236c5517 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -62,10 +62,11 @@ export class OAuthClientUsersService { )[0]; await this.userRepository.addToOAuthClient(user.id, oAuthClientId); const updatedUser = await this.userRepository.update(user.id, { - name: body.name ?? user.username ?? undefined, + name: body.name, locale: body.locale, }); user.locale = updatedUser.locale; + user.name = updatedUser.name; } const { accessToken, refreshToken, accessTokenExpiresAt } = await this.tokensRepository.createOAuthTokens( diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts index 0520556e7ff401..3a4acaf386e148 100644 --- a/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts @@ -18,6 +18,14 @@ export class OrganizationsTeamsRepository { }); } + async findTeamById(teamId: number) { + return this.dbRead.prisma.team.findUnique({ + where: { + id: teamId, + }, + }); + } + async findOrgTeams(organizationId: number) { return this.dbRead.prisma.team.findMany({ where: { diff --git a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts index e23bbe0e29587a..2eef2c4ccea2ee 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts @@ -105,7 +105,7 @@ export class OrganizationsEventTypesService { role: user.role, organizationId: user.organizationId, organization: { isOrgAdmin }, - profile: { id: profileId }, + profile: { id: profileId || null }, metadata: user.metadata, }; } diff --git a/apps/api/v2/src/modules/users/inputs/create-managed-user.input.ts b/apps/api/v2/src/modules/users/inputs/create-managed-user.input.ts index fdfa6d25f3ca14..7424dfa2879cec 100644 --- a/apps/api/v2/src/modules/users/inputs/create-managed-user.input.ts +++ b/apps/api/v2/src/modules/users/inputs/create-managed-user.input.ts @@ -11,8 +11,8 @@ export class CreateManagedUserInput { email!: string; @IsString() - @IsOptional() - name?: string; + @ApiProperty({ example: "Alice Smith", description: "Managed user's name is used in emails" }) + name!: string; @IsOptional() @ApiProperty({ example: 12, enum: [12, 24], description: "Must be 12 or 24" }) @@ -29,7 +29,11 @@ export class CreateManagedUserInput { @IsTimeZone() @IsOptional() @CapitalizeTimeZone() - @ApiProperty({ example: "America/New_York" }) + @ApiProperty({ + example: "America/New_York", + description: `Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have + a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.`, + }) timeZone?: string; @IsEnum(Locales) diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index f9f0083a6096a7..eb711680109843 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -597,14 +597,33 @@ }, "/v2/event-types": { "post": { - "operationId": "EventTypesController_2024_04_15_createEventType", - "parameters": [], + "operationId": "EventTypesController_2024_06_14_createEventType", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateEventTypeInput_2024_04_15" + "$ref": "#/components/schemas/CreateEventTypeInput_2024_06_14" } } } @@ -615,7 +634,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateEventTypeOutput" + "$ref": "#/components/schemas/CreateEventTypeOutput_2024_06_14" } } } @@ -626,15 +645,52 @@ ] }, "get": { - "operationId": "EventTypesController_2024_04_15_getEventTypes", - "parameters": [], + "operationId": "EventTypesController_2024_06_14_getEventTypes", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "username", + "required": true, + "in": "query", + "description": "The username of the user to get event types for. If only username provided will get all event types.", + "schema": { + "type": "string" + } + }, + { + "name": "eventSlug", + "required": true, + "in": "query", + "description": "Slug of event type to return. Notably, if eventSlug is provided then username must be provided too, because multiple users can have event with same slug.", + "schema": { + "type": "string" + } + }, + { + "name": "usernames", + "required": true, + "in": "query", + "description": "Get dynamic event type for multiple usernames separated by comma. e.g `usernames=alice,bob`", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetEventTypesOutput" + "$ref": "#/components/schemas/GetEventTypesOutput_2024_06_14" } } } @@ -647,14 +703,32 @@ }, "/v2/event-types/{eventTypeId}": { "get": { - "operationId": "EventTypesController_2024_04_15_getEventType", + "operationId": "EventTypesController_2024_06_14_getEventTypeById", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "eventTypeId", "required": true, "in": "path", "schema": { - "type": "number" + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" } } ], @@ -664,7 +738,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetEventTypeOutput" + "$ref": "#/components/schemas/GetEventTypeOutput_2024_06_14" } } } @@ -675,8 +749,17 @@ ] }, "patch": { - "operationId": "EventTypesController_2024_04_15_updateEventType", + "operationId": "EventTypesController_2024_06_14_updateEventType", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "eventTypeId", "required": true, @@ -684,6 +767,15 @@ "schema": { "type": "number" } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } } ], "requestBody": { @@ -691,7 +783,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateEventTypeInput_2024_04_15" + "$ref": "#/components/schemas/UpdateEventTypeInput_2024_06_14" } } } @@ -702,7 +794,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateEventTypeOutput" + "$ref": "#/components/schemas/UpdateEventTypeOutput_2024_06_14" } } } @@ -713,8 +805,17 @@ ] }, "delete": { - "operationId": "EventTypesController_2024_04_15_deleteEventType", + "operationId": "EventTypesController_2024_06_14_deleteEventType", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "eventTypeId", "required": true, @@ -722,6 +823,15 @@ "schema": { "type": "number" } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -730,7 +840,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteEventTypeOutput" + "$ref": "#/components/schemas/DeleteEventTypeOutput_2024_06_14" } } } @@ -816,91 +926,6 @@ ] } }, - "/v2/event-types/{username}/{eventSlug}/public": { - "get": { - "operationId": "EventTypesController_2024_04_15_getPublicEventType", - "parameters": [ - { - "name": "username", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "eventSlug", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "isTeamEvent", - "required": false, - "in": "query", - "schema": { - "type": "boolean" - } - }, - { - "name": "org", - "required": false, - "in": "query", - "schema": { - "nullable": true, - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetEventTypePublicOutput" - } - } - } - } - }, - "tags": [ - "Event types" - ] - } - }, - "/v2/event-types/{username}/public": { - "get": { - "operationId": "EventTypesController_2024_04_15_getPublicEventTypes", - "parameters": [ - { - "name": "username", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetEventTypesPublicOutput" - } - } - } - } - }, - "tags": [ - "Event types" - ] - } - }, "/v2/organizations/{orgId}/teams": { "get": { "operationId": "OrganizationsTeamsController_getAllTeams", @@ -1843,6 +1868,15 @@ "schema": { "type": "number" } + }, + { + "name": "eventSlug", + "required": true, + "in": "query", + "description": "Slug of team event type to return.", + "schema": { + "type": "string" + } } ], "responses": { @@ -3005,14 +3039,35 @@ }, "/v2/schedules": { "post": { - "operationId": "SchedulesController_2024_04_15_createSchedule", - "parameters": [], + "operationId": "SchedulesController_2024_06_11_createSchedule", + "summary": "Create a schedule", + "description": "\n The point of creating schedules is for event types to be available at specific times.\n\n First goal of schedules is to have a default schedule. If you are platform customer and created managed users, then it is important to note that each managed user should have a default schedule.\n 1. If you passed `timeZone` when creating managed user, then the default schedule from Monday to Friday from 9AM to 5PM will be created with that timezone. Managed user can then change the default schedule via `AvailabilitySettings` atom.\n 2. If you did not, then we assume you want that user has specific schedule right away. You should create default schedule by specifying\n `\"isDefault\": true` in the request body. Until the user has a default schedule that user can't be booked or manage his / her schedule via the AvailabilitySettings atom.\n\n Second goal is to create other schedules that event types can point to, so that when that event is booked availability is not checked against the default schedule but against that specific schedule.\n After creating a non default schedule you can update event type to point to that schedule via the PATCH `event-types/{eventTypeId}` endpoint.\n\n When specifying start time and end time for each day use 24 hour format e.g. 08:00, 15:00 etc.\n ", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateScheduleInput_2024_04_15" + "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" } } } @@ -3023,7 +3078,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateScheduleOutput_2024_04_15" + "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" } } } @@ -3034,15 +3089,36 @@ ] }, "get": { - "operationId": "SchedulesController_2024_04_15_getSchedules", - "parameters": [], + "operationId": "SchedulesController_2024_06_11_getSchedules", + "summary": "", + "description": "Returns all schedules of the authenticated user", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetSchedulesOutput_2024_04_15" + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" } } } @@ -3055,15 +3131,34 @@ }, "/v2/schedules/default": { "get": { - "operationId": "SchedulesController_2024_04_15_getDefaultSchedule", - "parameters": [], + "operationId": "SchedulesController_2024_06_11_getDefaultSchedule", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Returns the default schedule", + "description": "Returns the default schedule of the authenticated user", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_04_15" + "$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_06_11" } } } @@ -3076,8 +3171,26 @@ }, "/v2/schedules/{scheduleId}": { "get": { - "operationId": "SchedulesController_2024_04_15_getSchedule", + "operationId": "SchedulesController_2024_06_11_getSchedule", "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "scheduleId", "required": true, @@ -3093,7 +3206,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetScheduleOutput_2024_04_15" + "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" } } } @@ -3104,10 +3217,28 @@ ] }, "patch": { - "operationId": "SchedulesController_2024_04_15_updateSchedule", + "operationId": "SchedulesController_2024_06_11_updateSchedule", "parameters": [ { - "name": "scheduleId", + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduleId", "required": true, "in": "path", "schema": { @@ -3120,7 +3251,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15" + "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" } } } @@ -3131,7 +3262,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateScheduleOutput_2024_04_15" + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" } } } @@ -3142,8 +3273,26 @@ ] }, "delete": { - "operationId": "SchedulesController_2024_04_15_deleteSchedule", + "operationId": "SchedulesController_2024_06_11_deleteSchedule", "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "scheduleId", "required": true, @@ -3159,7 +3308,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteScheduleOutput_2024_04_15" + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" } } } @@ -3606,64 +3755,16 @@ } }, "/v2/bookings": { - "get": { - "operationId": "BookingsController_getBookings", - "parameters": [ - { - "name": "cursor", - "required": false, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "limit", - "required": false, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "filters[status]", - "required": true, - "in": "query", - "schema": { - "enum": [ - "upcoming", - "recurring", - "past", - "cancelled", - "unconfirmed" - ], - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetBookingsOutput" - } - } - } - } - }, - "tags": [ - "Bookings" - ] - }, "post": { - "operationId": "BookingsController_createBooking", + "operationId": "BookingsController_2024_08_13_createBooking", + "summary": "Create booking", + "description": "\n POST /v2/bookings is used to create regular bookings, recurring bookings and instant bookings. The request bodies for all 3 are almost the same except:\n If eventTypeId in the request body is id of a regular event, then regular booking is created.\n\n If it is an id of a recurring event type, then recurring booking is created.\n\n Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible.\n\n For team event types it is possible to create instant meeting. To do that just pass `\"instant\": true` to the request body.\n \n The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone.\n ", "parameters": [ { - "name": "x-cal-client-id", - "required": true, + "name": "cal-api-version", "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, "schema": { "type": "string" } @@ -3671,10 +3772,21 @@ ], "requestBody": { "required": true, + "description": "Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBookingInput" + "oneOf": [ + { + "$ref": "#/components/schemas/CreateBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateInstantBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateRecurringBookingInput_2024_08_13" + } + ] } } } @@ -3685,7 +3797,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" } } } @@ -3694,16 +3806,186 @@ "tags": [ "Bookings" ] - } - }, - "/v2/bookings/{bookingUid}": { + }, "get": { - "operationId": "BookingsController_getBooking", + "operationId": "BookingsController_2024_08_13_getBookings", "parameters": [ { - "name": "bookingUid", + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.", + "example": "?status=upcoming,past", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "upcoming", + "recurring", + "past", + "cancelled", + "unconfirmed" + ] + } + } + }, + { + "name": "attendeeEmail", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's email address.", + "example": "example@domain.com", + "schema": { + "type": "string" + } + }, + { + "name": "attendeeName", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's name.", + "example": "John Doe", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeIds", + "required": false, + "in": "query", + "description": "Filter bookings by event type ids belonging to the user. Event type ids must be separated by a comma.", + "example": "?eventTypeIds=100,200", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": false, + "in": "query", + "description": "Filter bookings by event type id belonging to the user.", + "example": "?eventTypeId=100", + "schema": { + "type": "string" + } + }, + { + "name": "teamsIds", + "required": false, + "in": "query", + "description": "Filter bookings by team ids that user is part of. Team ids must be separated by a comma.", + "example": "?teamIds=50,60", + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": false, + "in": "query", + "description": "Filter bookings by team id that user is part of", + "example": "?teamId=50", + "schema": { + "type": "string" + } + }, + { + "name": "afterStart", + "required": false, + "in": "query", + "description": "Filter bookings with start after this date string.", + "example": "?afterStart=2025-03-07T10:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "beforeEnd", + "required": false, + "in": "query", + "description": "Filter bookings with end before this date string.", + "example": "?beforeEnd=2025-03-07T11:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortCreated", + "required": false, + "in": "query", + "description": "Sort results by their creation time (when booking was made) in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", "required": true, - "in": "path", "schema": { "type": "string" } @@ -3715,7 +3997,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBookingOutput" + "$ref": "#/components/schemas/GetBookingsOutput_2024_08_13" } } } @@ -3726,10 +4008,21 @@ ] } }, - "/v2/bookings/{bookingUid}/reschedule": { + "/v2/bookings/{bookingUid}": { "get": { - "operationId": "BookingsController_getBookingForReschedule", + "operationId": "BookingsController_2024_08_13_getBooking", + "summary": "Get booking", + "description": "`:bookingUid` can be\n \n 1. uid of a normal booking\n \n 2. uid of one of the recurring booking recurrences\n \n 3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "bookingUid", "required": true, @@ -3745,7 +4038,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GetBookingOutput_2024_08_13" } } } @@ -3756,22 +4049,25 @@ ] } }, - "/v2/bookings/{bookingId}/cancel": { + "/v2/bookings/{bookingUid}/reschedule": { "post": { - "operationId": "BookingsController_cancelBooking", + "operationId": "BookingsController_2024_08_13_rescheduleBooking", + "summary": "Reschedule booking", + "description": "Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.", "parameters": [ { - "name": "bookingId", + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", "required": true, - "in": "path", "schema": { "type": "string" } }, { - "name": "x-cal-client-id", + "name": "bookingUid", "required": true, - "in": "header", + "in": "path", "schema": { "type": "string" } @@ -3782,7 +4078,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CancelBookingInput" + "$ref": "#/components/schemas/RescheduleBookingInput_2024_08_13" } } } @@ -3793,7 +4089,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/RescheduleBookingOutput_2024_08_13" } } } @@ -3804,10 +4100,19 @@ ] } }, - "/v2/bookings/{bookingUid}/mark-no-show": { + "/v2/bookings/{bookingUid}/cancel": { "post": { - "operationId": "BookingsController_markNoShow", + "operationId": "BookingsController_2024_08_13_cancelBooking", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "bookingUid", "required": true, @@ -3822,18 +4127,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MarkNoShowInput" + "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" } } } }, "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MarkNoShowOutput" + "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" } } } @@ -3844,14 +4149,32 @@ ] } }, - "/v2/bookings/recurring": { + "/v2/bookings/{bookingUid}/mark-absent": { "post": { - "operationId": "BookingsController_createRecurringBooking", + "operationId": "BookingsController_2024_08_13_markNoShow", "parameters": [ { - "name": "x-cal-client-id", + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, "schema": { "type": "string" } @@ -3862,61 +4185,18 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - }, - "tags": [ - "Bookings" - ] - } - }, - "/v2/bookings/instant": { - "post": { - "operationId": "BookingsController_createInstantBooking", - "parameters": [ - { - "name": "x-cal-client-id", - "required": true, - "in": "header", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBookingInput" + "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" } } } }, "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" } } } @@ -4755,6 +5035,11 @@ "type": "string", "example": "alice@example.com" }, + "name": { + "type": "string", + "example": "Alice Smith", + "description": "Managed user's name is used in emails" + }, "timeFormat": { "type": "number", "example": 12, @@ -4779,7 +5064,8 @@ }, "timeZone": { "type": "string", - "example": "America/New_York" + "example": "America/New_York", + "description": "Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have\n a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked." }, "locale": { "enum": [ @@ -4828,13 +5114,11 @@ ], "type": "string", "example": "en" - }, - "name": { - "type": "string" } }, "required": [ - "email" + "email", + "name" ] }, "CreateManagedUserData": { @@ -5236,1972 +5520,1910 @@ "refreshToken" ] }, - "CreateEventTypeInput_2024_06_14": { + "AddressLocation_2024_06_14": { "type": "object", "properties": { - "lengthInMinutes": { - "type": "number", - "example": 60 - }, - "title": { + "type": { "type": "string", - "example": "Learn the secrets of masterchief!" + "example": "address", + "description": "only allowed value for type is `address`" }, - "description": { + "address": { "type": "string", - "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + "example": "123 Example St, City, Country" + }, + "public": { + "type": "boolean" } }, "required": [ - "lengthInMinutes", - "title", - "description" + "type", + "address", + "public" ] }, - "EventTypeOutput_2024_06_14": { + "LinkLocation_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1 + "type": { + "type": "string", + "example": "link", + "description": "only allowed value for type is `link`" + }, + "link": { + "type": "string", + "example": "https://customvideo.com/join/123456" + }, + "public": { + "type": "boolean" } }, "required": [ - "id" + "type", + "link", + "public" ] }, - "CreateEventTypeOutput_2024_06_14": { + "IntegrationLocation_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "enum": [ - "success", - "error" - ], - "example": "success" + "example": "integration", + "description": "only allowed value for type is `integration`" }, - "data": { - "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + "integration": { + "type": "string", + "example": "cal-video", + "enum": [ + "cal-video" + ] } }, "required": [ - "status", - "data" + "type", + "integration" ] }, - "GetEventTypeOutput_2024_06_14": { + "PhoneLocation_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "enum": [ - "success", - "error" - ], - "example": "success" + "example": "phone", + "description": "only allowed value for type is `phone`" }, - "data": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" - } - ] + "phone": { + "type": "string", + "example": "+37120993151" + }, + "public": { + "type": "boolean" } }, "required": [ - "status", - "data" + "type", + "phone", + "public" ] }, - "GetEventTypesOutput_2024_06_14": { + "PhoneFieldInput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "enum": [ - "success", - "error" - ], - "example": "success" + "example": "phone", + "description": "only allowed value for type is `phone`" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" - } + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string" } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "placeholder" ] }, - "UpdateEventTypeInput_2024_06_14": { - "type": "object", - "properties": {} - }, - "UpdateEventTypeOutput_2024_06_14": { + "AddressFieldInput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "enum": [ - "success", - "error" - ], - "example": "success" + "example": "address", + "description": "only allowed value for type is `address`" }, - "data": { - "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter your address" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 1234 Main St" } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "placeholder" ] }, - "DeleteData_2024_06_14": { + "TextFieldInput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1 + "type": { + "type": "string", + "example": "text", + "description": "only allowed value for type is `text`" }, - "lengthInMinutes": { - "type": "number", - "example": 60 + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "title": { + "label": { "type": "string", - "example": "Learn the secrets of masterchief!" + "example": "Please enter your text" }, - "slug": { - "type": "string" + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Enter text here" } }, "required": [ - "id", - "lengthInMinutes", - "title", - "slug" + "type", + "slug", + "label", + "required", + "placeholder" ] }, - "DeleteEventTypeOutput_2024_06_14": { + "NumberFieldInput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "enum": [ - "success", - "error" - ], - "example": "success" + "example": "number", + "description": "only allowed value for type is `number`" }, - "data": { - "$ref": "#/components/schemas/DeleteData_2024_06_14" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter a number" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 100" } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "placeholder" ] }, - "SelectedCalendarsInputDto": { + "TextAreaFieldInput_2024_06_14": { "type": "object", "properties": { - "integration": { - "type": "string" + "type": { + "type": "string", + "example": "textarea", + "description": "only allowed value for type is `textarea`" }, - "externalId": { - "type": "string" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "credentialId": { - "type": "number" + "label": { + "type": "string", + "example": "Please enter detailed information" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Detailed description here..." } }, "required": [ - "integration", - "externalId", - "credentialId" + "type", + "slug", + "label", + "required", + "placeholder" ] }, - "SelectedCalendarOutputDto": { + "SelectFieldInput_2024_06_14": { "type": "object", "properties": { - "userId": { - "type": "number" + "type": { + "type": "string", + "example": "select", + "description": "only allowed value for type is `select`" }, - "integration": { - "type": "string" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "externalId": { - "type": "string" + "label": { + "type": "string", + "example": "Please select an option" }, - "credentialId": { - "type": "number", - "nullable": true + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "Select..." + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "userId", - "integration", - "externalId", - "credentialId" + "type", + "slug", + "label", + "required", + "placeholder", + "options" ] }, - "SelectedCalendarOutputResponseDto": { + "MultiSelectFieldInput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "multiselect", + "description": "only allowed value for type is `multiselect`" }, - "data": { - "$ref": "#/components/schemas/SelectedCalendarOutputDto" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please select multiple options" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "options" ] }, - "EventTypeLocation_2024_04_15": { + "MultiEmailFieldInput_2024_06_14": { "type": "object", "properties": { "type": { "type": "string", - "example": "link" + "example": "multiemail", + "description": "only allowed value for type is `multiemail`" }, - "link": { + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter multiple emails" + }, + "required": { + "type": "boolean" + }, + "placeholder": { "type": "string", - "example": "https://masterchief.com/argentina/flan/video/9129412" + "example": "e.g., example@example.com" } }, "required": [ - "type" + "type", + "slug", + "label", + "required", + "placeholder" ] }, - "CreateEventTypeInput_2024_04_15": { + "CheckboxGroupFieldInput_2024_06_14": { "type": "object", "properties": { - "length": { - "type": "number", - "minimum": 1, - "example": 60 + "type": { + "type": "string", + "example": "checkbox", + "description": "only allowed value for type is `checkbox`" }, "slug": { "type": "string", - "example": "cooking-class" + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "title": { + "label": { "type": "string", - "example": "Learn the secrets of masterchief!" + "example": "Select all that apply" }, - "description": { - "type": "string", - "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + "required": { + "type": "boolean" }, - "locations": { + "options": { + "example": [ + "Checkbox 1", + "Checkbox 2" + ], "type": "array", "items": { - "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + "type": "string" } - }, - "disableGuests": { - "type": "boolean" - }, - "slotInterval": { - "type": "number", - "minimum": 0 - }, - "minimumBookingNotice": { - "type": "number", - "minimum": 0 - }, - "beforeEventBuffer": { - "type": "number", - "minimum": 0 - }, - "afterEventBuffer": { - "type": "number", - "minimum": 0 } }, "required": [ - "length", + "type", "slug", - "title" + "label", + "required", + "options" ] }, - "EventTypeOutput": { + "RadioGroupFieldInput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1 - }, - "length": { - "type": "number", - "example": 60 + "type": { + "type": "string", + "example": "radio", + "description": "only allowed value for type is `radio`" }, "slug": { "type": "string", - "example": "cooking-class" + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "title": { + "label": { "type": "string", - "example": "Learn the secrets of masterchief!" + "example": "Select one option" }, - "description": { - "type": "string", - "nullable": true, - "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + "required": { + "type": "boolean" }, - "locations": { - "nullable": true, + "options": { + "example": [ + "Radio 1", + "Radio 2" + ], "type": "array", "items": { - "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + "type": "string" } } }, "required": [ - "id", - "length", + "type", "slug", - "title", - "description", - "locations" + "label", + "required", + "options" ] }, - "CreateEventTypeOutput": { + "BooleanFieldInput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "boolean", + "description": "only allowed value for type is `boolean`" }, - "data": { - "$ref": "#/components/schemas/EventTypeOutput" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Agree to terms?" + }, + "required": { + "type": "boolean" } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required" ] }, - "Data": { + "BusinessDaysWindow_2024_06_14": { "type": "object", "properties": { - "eventType": { - "$ref": "#/components/schemas/EventTypeOutput" + "type": { + "type": "string", + "enum": [ + "businessDays", + "calendarDays", + "range" + ], + "description": "Whether the window should be business days, calendar days or a range of dates" + }, + "value": { + "type": "number", + "example": 5, + "description": "How many business day into the future can this event be booked" + }, + "rolling": { + "type": "boolean", + "example": true, + "description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date." } }, "required": [ - "eventType" + "type", + "value", + "rolling" ] }, - "GetEventTypeOutput": { + "CalendarDaysWindow_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] + "businessDays", + "calendarDays", + "range" + ], + "description": "Whether the window should be business days, calendar days or a range of dates" }, - "data": { - "$ref": "#/components/schemas/Data" + "value": { + "type": "number", + "example": 5, + "description": "How many calendar days into the future can this event be booked" + }, + "rolling": { + "type": "boolean", + "example": true, + "description": "If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date." } }, "required": [ - "status", - "data" + "type", + "value", + "rolling" ] }, - "EventTypeGroup": { + "RangeWindow_2024_06_14": { "type": "object", "properties": { - "eventTypes": { + "type": { + "type": "string", + "enum": [ + "businessDays", + "calendarDays", + "range" + ], + "description": "Whether the window should be business days, calendar days or a range of dates" + }, + "value": { + "example": [ + "2030-09-05", + "2030-09-09" + ], + "description": "Date range for when this event can be booked.", "type": "array", "items": { - "$ref": "#/components/schemas/EventTypeOutput" + "type": "string" } } }, "required": [ - "eventTypes" + "type", + "value" ] }, - "GetEventTypesData": { + "BookingLimitsCount_2024_06_14": { "type": "object", "properties": { - "eventTypeGroups": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventTypeGroup" - } + "day": { + "type": "number", + "description": "The number of bookings per day", + "example": 1 + }, + "week": { + "type": "number", + "description": "The number of bookings per week", + "example": 2 + }, + "month": { + "type": "number", + "description": "The number of bookings per month", + "example": 3 + }, + "year": { + "type": "number", + "description": "The number of bookings per year", + "example": 4 } }, "required": [ - "eventTypeGroups" + "day", + "week", + "month", + "year" ] }, - "GetEventTypesOutput": { + "BookingLimitsDuration_2024_06_14": { "type": "object", "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "day": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per day (must be a multiple of 15)", + "example": 60 }, - "data": { - "$ref": "#/components/schemas/GetEventTypesData" + "week": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per week (must be a multiple of 15)", + "example": 120 + }, + "month": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per month (must be a multiple of 15)", + "example": 180 + }, + "year": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per year (must be a multiple of 15)", + "example": 240 } - }, - "required": [ - "status", - "data" - ] + } }, - "Location": { - "type": "object", - "properties": { - "type": { - "type": "string" - } - }, - "required": [ - "type" - ] - }, - "Source": { + "Recurrence_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "string" + "interval": { + "type": "number", + "example": 10, + "description": "Repeats every {count} week | month | year" }, - "type": { - "type": "string" + "occurrences": { + "type": "number", + "example": 10, + "description": "Repeats for a maximum of {count} events" }, - "label": { - "type": "string" + "frequency": { + "type": "string", + "enum": [ + "yearly", + "monthly", + "weekly" + ] } }, "required": [ - "id", - "type", - "label" + "interval", + "occurrences", + "frequency" ] }, - "BookingField": { + "CreateEventTypeInput_2024_06_14": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "type": { - "type": "string" - }, - "defaultLabel": { - "type": "string" - }, - "label": { - "type": "string" - }, - "placeholder": { - "type": "string" - }, - "required": { - "type": "boolean" + "lengthInMinutes": { + "type": "number", + "example": 60 }, - "getOptionsAt": { - "type": "string" + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" }, - "hideWhenJustOneOption": { - "type": "boolean" + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" }, - "editable": { - "type": "string" + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" }, - "sources": { + "locations": { "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", "items": { - "$ref": "#/components/schemas/Source" + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] } }, - "disableOnPrefill": { - "type": "boolean" - } - }, - "required": [ - "name", - "type" - ] - }, - "Organization": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "slug": { - "type": "string", - "nullable": true - }, - "name": { - "type": "string" + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } }, - "metadata": { - "type": "object" - } - }, - "required": [ - "id", - "name", - "metadata" - ] - }, - "Profile": { - "type": "object", - "properties": { - "username": { - "type": "string", - "nullable": true + "disableGuests": { + "type": "boolean", + "description": "If true, person booking this event't cant add guests via their emails." }, - "id": { + "slotInterval": { "type": "number", - "nullable": true + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." }, - "userId": { - "type": "number" + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." }, - "uid": { - "type": "string" + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." }, - "name": { - "type": "string" + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." }, - "organizationId": { + "scheduleId": { "type": "number", - "nullable": true + "description": "If you want that this event has different schedule than user's default one you can specify it here." }, - "organization": { - "nullable": true, + "bookingLimitsCount": { + "description": "Limit how many times this event can be booked", "allOf": [ { - "$ref": "#/components/schemas/Organization" + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" } ] }, - "upId": { - "type": "string" - }, - "image": { - "type": "string" + "onlyShowFirstAvailableSlot": { + "type": "boolean", + "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." }, - "brandColor": { - "type": "string" + "bookingLimitsDuration": { + "description": "Limit total amount of time that this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + } + ] }, - "darkBrandColor": { - "type": "string" + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } }, - "theme": { - "type": "string" + "offsetStart": { + "type": "number", + "description": "Offset timeslots shown to bookers by a specified number of minutes" }, - "bookerLayouts": { - "type": "object" + "recurrence": { + "description": "Create a recurring event that can be booked once but will occur multiple times", + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] } }, "required": [ - "username", - "id", - "organizationId", - "upId" + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence" ] }, - "Owner": { + "EmailDefaultFieldOutput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "avatarUrl": { - "type": "string", - "nullable": true - }, - "username": { - "type": "string", - "nullable": true + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true }, - "name": { + "slug": { "type": "string", - "nullable": true - }, - "weekStart": { - "type": "string" + "default": "email" }, - "brandColor": { + "type": { "type": "string", - "nullable": true + "default": "email" }, - "darkBrandColor": { - "type": "string", - "nullable": true + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "NameDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true }, - "theme": { + "slug": { "type": "string", - "nullable": true - }, - "metadata": { - "type": "object" + "default": "name" }, - "defaultScheduleId": { - "type": "number", - "nullable": true - }, - "nonProfileUsername": { + "type": { "type": "string", - "nullable": true + "default": "name" }, - "profile": { - "$ref": "#/components/schemas/Profile" + "required": { + "type": "boolean" } }, "required": [ - "id", - "username", - "name", - "weekStart", - "metadata", - "nonProfileUsername", - "profile" + "isDefault", + "slug", + "type", + "required" ] }, - "Schedule": { + "LocationDefaultFieldOutput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number" + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true }, - "timeZone": { + "slug": { "type": "string", - "nullable": true + "default": "location" + }, + "type": { + "type": "string", + "default": "radioInput" + }, + "required": { + "type": "boolean" } }, "required": [ - "id", - "timeZone" + "isDefault", + "slug", + "type", + "required" ] }, - "User": { + "RescheduleReasonDefaultFieldOutput_2024_06_14": { "type": "object", "properties": { - "username": { - "type": "string", - "nullable": true + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true }, - "name": { + "slug": { "type": "string", - "nullable": true - }, - "weekStart": { - "type": "string" - }, - "organizationId": { - "type": "number" + "default": "rescheduleReason" }, - "avatarUrl": { + "type": { "type": "string", - "nullable": true - }, - "profile": { - "$ref": "#/components/schemas/Profile" + "default": "textarea" }, - "bookerUrl": { - "type": "string" + "required": { + "type": "boolean" } }, "required": [ - "username", - "name", - "weekStart", - "profile", - "bookerUrl" + "isDefault", + "slug", + "type", + "required" ] }, - "PublicEventTypeOutput": { + "TitleDefaultFieldOutput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "eventName": { - "type": "string", - "nullable": true + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true }, "slug": { - "type": "string" - }, - "isInstantEvent": { - "type": "boolean" - }, - "aiPhoneCallConfig": { - "type": "object" - }, - "schedulingType": { - "type": "object" - }, - "length": { - "type": "number" - }, - "locations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Location" - } + "type": "string", + "default": "title" }, - "customInputs": { - "type": "array", - "items": { - "type": "object" - } + "type": { + "type": "string", + "default": "text" }, - "disableGuests": { + "required": { "type": "boolean" - }, - "metadata": { + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "NotesDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { "type": "object", - "nullable": true - }, - "lockTimeZoneToggleOnBookingPage": { - "type": "boolean" - }, - "requiresConfirmation": { - "type": "boolean" - }, - "requiresBookerEmailVerification": { - "type": "boolean" - }, - "recurringEvent": { - "type": "object" - }, - "price": { - "type": "number" - }, - "currency": { - "type": "string" - }, - "seatsPerTimeSlot": { - "type": "number", - "nullable": true - }, - "seatsShowAvailabilityCount": { - "type": "boolean", - "nullable": true - }, - "bookingFields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BookingField" - } - }, - "team": { - "type": "object" + "default": true, + "description": "This property is always true because it's a default field", + "example": true }, - "successRedirectUrl": { + "slug": { "type": "string", - "nullable": true - }, - "workflows": { - "type": "array", - "items": { - "type": "object" - } - }, - "hosts": { - "type": "array", - "items": { - "type": "object" - } - }, - "owner": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/Owner" - } - ] - }, - "schedule": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/Schedule" - } - ] - }, - "hidden": { - "type": "boolean" - }, - "assignAllTeamMembers": { - "type": "boolean" - }, - "bookerLayouts": { - "type": "object" + "default": "notes" }, - "users": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - }, - "entity": { - "type": "object" + "type": { + "type": "string", + "default": "textarea" }, - "isDynamic": { + "required": { "type": "boolean" } }, "required": [ - "id", - "title", - "description", + "isDefault", "slug", - "isInstantEvent", - "length", - "locations", - "customInputs", - "disableGuests", - "metadata", - "lockTimeZoneToggleOnBookingPage", - "requiresConfirmation", - "requiresBookerEmailVerification", - "price", - "currency", - "seatsShowAvailabilityCount", - "bookingFields", - "workflows", - "hosts", - "owner", - "schedule", - "hidden", - "assignAllTeamMembers", - "users", - "entity", - "isDynamic" + "type", + "required" ] }, - "GetEventTypePublicOutput": { + "GuestsDefaultFieldOutput_2024_06_14": { "type": "object", "properties": { - "status": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "default": "guests" }, - "data": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/PublicEventTypeOutput" - } - ] + "type": { + "type": "string", + "default": "multiemail" + }, + "required": { + "type": "boolean" } }, "required": [ - "status", - "data" + "isDefault", + "slug", + "type", + "required" ] }, - "PublicEventType": { + "AddressFieldOutput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1 - }, - "length": { - "type": "number", - "example": 60 + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "address", + "description": "only allowed value for type is `address`" }, "slug": { "type": "string", - "example": "cooking-class" + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "title": { + "label": { "type": "string", - "example": "Learn the secrets of masterchief!" + "example": "Please enter your address" }, - "description": { + "required": { + "type": "boolean" + }, + "placeholder": { "type": "string", - "nullable": true + "example": "e.g., 1234 Main St" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "id", - "length", + "type", "slug", - "title" + "label", + "required", + "isDefault" ] }, - "GetEventTypesPublicOutput": { + "BooleanFieldOutput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "boolean", + "description": "only allowed value for type is `boolean`" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PublicEventType" - } - } - }, - "required": [ - "status", - "data" - ] - }, - "Option": { - "type": "object", - "properties": { - "value": { - "type": "string" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, "label": { - "type": "string" - } - }, - "required": [ - "value", - "label" - ] - }, - "VariantsConfig": { - "type": "object", - "properties": { - "variants": { - "type": "object" - } - }, - "required": [ - "variants" - ] - }, - "View": { - "type": "object", - "properties": { - "id": { - "type": "string" + "type": "string", + "example": "Agree to terms?" }, - "label": { - "type": "string" + "required": { + "type": "boolean" }, - "description": { - "type": "string" + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "id", - "label" + "type", + "slug", + "label", + "required", + "isDefault" ] }, - "BookingField_2024_04_15": { + "CheckboxGroupFieldOutput_2024_06_14": { "type": "object", "properties": { "type": { "type": "string", "enum": [ - "number", - "boolean", + "phone", "address", - "name", "text", + "number", "textarea", - "email", - "phone", - "multiemail", "select", "multiselect", + "multiemail", "checkbox", "radio", - "radioInput" - ] - }, - "name": { - "type": "string" + "boolean" + ], + "example": "checkbox", + "description": "only allowed value for type is `checkbox`" }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Option" - } + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, "label": { - "type": "string" - }, - "labelAsSafeHtml": { - "type": "string" - }, - "defaultLabel": { - "type": "string" - }, - "placeholder": { - "type": "string" + "type": "string", + "example": "Select all that apply" }, "required": { "type": "boolean" }, - "getOptionsAt": { - "type": "string" - }, - "optionsInputs": { - "type": "object" - }, - "variant": { - "type": "string" - }, - "variantsConfig": { - "$ref": "#/components/schemas/VariantsConfig" - }, - "views": { - "type": "array", - "items": { - "$ref": "#/components/schemas/View" - } - }, - "hideWhenJustOneOption": { - "type": "boolean" - }, - "hidden": { - "type": "boolean" - }, - "editable": { - "type": "string", - "enum": [ - "system", - "system-but-optional", - "system-but-hidden", - "user", - "user-readonly" - ] - }, - "sources": { + "options": { + "example": [ + "Checkbox 1", + "Checkbox 2" + ], "type": "array", "items": { - "$ref": "#/components/schemas/Source" + "type": "string" } }, - "disableOnPrefill": { - "type": "boolean" + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ "type", - "name" + "slug", + "label", + "required", + "options", + "isDefault" ] }, - "UpdateEventTypeInput_2024_04_15": { + "MultiEmailFieldOutput_2024_06_14": { "type": "object", "properties": { - "length": { - "type": "number", - "minimum": 1 + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "multiemail", + "description": "only allowed value for type is `multiemail`" }, "slug": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "hidden": { - "type": "boolean" - }, - "locations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" - } + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "bookingFields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BookingField_2024_04_15" - } + "label": { + "type": "string", + "example": "Please enter multiple emails" }, - "disableGuests": { + "required": { "type": "boolean" }, - "minimumBookingNotice": { - "type": "number", - "minimum": 0 - }, - "beforeEventBuffer": { - "type": "number", - "minimum": 0 - }, - "afterEventBuffer": { - "type": "number", - "minimum": 0 - }, - "slotInterval": { - "type": "number", - "minimum": 0 - } - } - }, - "UpdateEventTypeOutput": { - "type": "object", - "properties": { - "status": { + "placeholder": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "e.g., example@example.com" }, - "data": { - "$ref": "#/components/schemas/EventTypeOutput" + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "isDefault" ] }, - "DeleteData": { + "MultiSelectFieldOutput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number", - "example": 1 - }, - "length": { - "type": "number", - "example": 60 + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "multiselect", + "description": "only allowed value for type is `multiselect`" }, "slug": { "type": "string", - "example": "cooking-class" + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "title": { + "label": { "type": "string", - "example": "Learn the secrets of masterchief!" + "example": "Please select multiple options" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "id", - "length", + "type", "slug", - "title" + "label", + "required", + "options", + "isDefault" ] }, - "DeleteEventTypeOutput": { + "NumberFieldOutput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "number", + "description": "only allowed value for type is `number`" }, - "data": { - "$ref": "#/components/schemas/DeleteData" - } - }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter a number" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 100" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "isDefault" ] }, - "OrgTeamOutputDto": { + "PhoneFieldOutput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "parentId": { - "type": "number" - }, - "name": { + "type": { "type": "string", - "minLength": 1 + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "phone", + "description": "only allowed value for type is `phone`" }, "slug": { - "type": "string" - }, - "logoUrl": { - "type": "string" - }, - "calVideoLogo": { - "type": "string" - }, - "appLogo": { - "type": "string" - }, - "appIconLogo": { - "type": "string" + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "bio": { + "label": { "type": "string" }, - "hideBranding": { - "type": "boolean" - }, - "isOrganization": { - "type": "boolean" - }, - "isPrivate": { + "required": { "type": "boolean" }, - "hideBookATeamMember": { - "type": "boolean", - "default": false - }, - "metadata": { - "type": "string" - }, - "theme": { - "type": "string" - }, - "brandColor": { - "type": "string" - }, - "darkBrandColor": { - "type": "string" - }, - "bannerUrl": { + "placeholder": { "type": "string" }, - "timeFormat": { - "type": "number" - }, - "timeZone": { - "type": "string", - "default": "Europe/London" - }, - "weekStart": { - "type": "string", - "default": "Sunday" + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "id", - "name" + "type", + "slug", + "label", + "required", + "isDefault" ] }, - "OrgTeamsOutputResponseDto": { + "RadioGroupFieldOutput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "radio", + "description": "only allowed value for type is `radio`" }, - "data": { + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Select one option" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Radio 1", + "Radio 2" + ], "type": "array", "items": { - "$ref": "#/components/schemas/OrgTeamOutputDto" + "type": "string" } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "options", + "isDefault" ] }, - "OrgMeTeamsOutputResponseDto": { + "SelectFieldOutput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "select", + "description": "only allowed value for type is `select`" }, - "data": { + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please select an option" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "Select..." + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], "type": "array", "items": { - "$ref": "#/components/schemas/OrgTeamOutputDto" + "type": "string" } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "options", + "isDefault" ] }, - "OrgTeamOutputResponseDto": { + "TextAreaFieldOutput_2024_06_14": { "type": "object", "properties": { - "status": { + "type": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "textarea", + "description": "only allowed value for type is `textarea`" }, - "data": { - "$ref": "#/components/schemas/OrgTeamOutputDto" + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter detailed information" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Detailed description here..." + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false } }, "required": [ - "status", - "data" + "type", + "slug", + "label", + "required", + "isDefault" ] }, - "UpdateOrgTeamDto": { + "TextFieldOutput_2024_06_14": { "type": "object", "properties": { - "name": { + "type": { "type": "string", - "minLength": 1 + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "text", + "description": "only allowed value for type is `text`" }, "slug": { - "type": "string" - }, - "logoUrl": { - "type": "string" + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" }, - "calVideoLogo": { - "type": "string" + "label": { + "type": "string", + "example": "Please enter your text" }, - "appLogo": { - "type": "string" + "required": { + "type": "boolean" }, - "appIconLogo": { - "type": "string" + "placeholder": { + "type": "string", + "example": "e.g., Enter text here" }, - "bio": { - "type": "string" + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "EventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 }, - "hideBranding": { - "type": "boolean", - "default": false + "lengthInMinutes": { + "type": "number", + "example": 60 }, - "isPrivate": { - "type": "boolean" + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" }, - "hideBookATeamMember": { - "type": "boolean" + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" }, - "metadata": { - "type": "string" + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" }, - "theme": { - "type": "string" + "locations": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } }, - "brandColor": { - "type": "string" + "bookingFields": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/NameDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/EmailDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/LocationDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TitleDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NotesDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/GuestsDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldOutput_2024_06_14" + } + ] + } }, - "darkBrandColor": { - "type": "string" + "disableGuests": { + "type": "boolean" }, - "bannerUrl": { - "type": "string" + "slotInterval": { + "type": "number", + "example": 60 }, - "timeFormat": { - "type": "number" + "minimumBookingNotice": { + "type": "number", + "example": 0 }, - "timeZone": { - "type": "string", - "default": "Europe/London" + "beforeEventBuffer": { + "type": "number", + "example": 0 }, - "weekStart": { - "type": "string", - "default": "Sunday" - } - } - }, - "CreateOrgTeamDto": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1 + "afterEventBuffer": { + "type": "number", + "example": 0 }, - "slug": { - "type": "string" + "recurrence": { + "type": "object" }, - "logoUrl": { - "type": "string" + "metadata": { + "type": "object" }, - "calVideoLogo": { - "type": "string" + "requiresConfirmation": { + "type": "boolean" }, - "appLogo": { - "type": "string" + "price": { + "type": "number" }, - "appIconLogo": { + "currency": { "type": "string" }, - "bio": { - "type": "string" + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" }, - "hideBranding": { - "type": "boolean", - "default": false + "seatsPerTimeSlot": { + "type": "object" }, - "isPrivate": { - "type": "boolean" + "forwardParamsSuccessRedirect": { + "type": "object" }, - "hideBookATeamMember": { - "type": "boolean" + "successRedirectUrl": { + "type": "object" }, - "metadata": { - "type": "string" + "seatsShowAvailabilityCount": { + "type": "object" }, - "theme": { - "type": "string" + "scheduleId": { + "type": "object" }, - "brandColor": { - "type": "string" + "bookingLimitsCount": { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" }, - "darkBrandColor": { - "type": "string" + "onlyShowFirstAvailableSlot": { + "type": "boolean" }, - "bannerUrl": { - "type": "string" + "bookingLimitsDuration": { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" }, - "timeFormat": { - "type": "number" + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } }, - "timeZone": { - "type": "string", - "default": "Europe/London" + "offsetStart": { + "type": "number" }, - "weekStart": { - "type": "string", - "default": "Sunday" + "ownerId": { + "type": "number", + "example": 10 }, - "autoAcceptCreator": { - "type": "boolean", - "default": true + "users": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "name" + "id", + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "recurrence", + "metadata", + "requiresConfirmation", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "seatsPerTimeSlot", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "seatsShowAvailabilityCount", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "ownerId", + "users" ] }, - "ScheduleAvailabilityInput_2024_06_11": { + "CreateEventTypeOutput_2024_06_14": { "type": "object", "properties": { - "days": { - "example": [ - "Monday", - "Tuesday" - ], - "type": "array", - "items": { - "type": "object" - } - }, - "startTime": { + "status": { "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "09:00" + "enum": [ + "success", + "error" + ], + "example": "success" }, - "endTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "10:00" + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" } }, "required": [ - "days", - "startTime", - "endTime" + "status", + "data" ] }, - "ScheduleOverrideInput_2024_06_11": { - "type": "object", - "properties": { - "date": { - "type": "string", - "example": "2024-05-20" - }, - "startTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "12:00" - }, - "endTime": { - "type": "string", - "pattern": "TIME_FORMAT_HH_MM", - "example": "13:00" - } - }, - "required": [ - "date", - "startTime", - "endTime" - ] - }, - "ScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 254 - }, - "ownerId": { - "type": "number", - "example": 478 - }, - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "13:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - }, - "required": [ - "id", - "ownerId", - "name", - "timeZone", - "availability", - "isDefault", - "overrides" - ] - }, - "GetSchedulesOutput_2024_06_11": { + "GetEventTypeOutput_2024_06_14": { "type": "object", "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - }, - "error": { - "type": "object" - } - }, - "required": [ - "status", - "data" - ] - }, - "CreateScheduleInput_2024_06_11": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - }, - "required": [ - "name", - "timeZone", - "isDefault" - ] - }, - "CreateScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - }, - "required": [ - "status", - "data" - ] - }, - "GetScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "success" }, "data": { "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - } - ] - }, - "error": { - "type": "object" - } - }, - "required": [ - "status", - "data" - ] - }, - "UpdateScheduleInput_2024_06_11": { - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "One-on-one coaching" - }, - "timeZone": { - "type": "string", - "example": "Europe/Rome" - }, - "availability": { - "example": [ - { - "days": [ - "Monday", - "Tuesday" - ], - "startTime": "09:00", - "endTime": "10:00" - } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" - } - }, - "isDefault": { - "type": "boolean", - "example": true - }, - "overrides": { - "example": [ - { - "date": "2024-05-20", - "startTime": "12:00", - "endTime": "14:00" + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" } - ], - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" - } - } - } - }, - "UpdateScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" - }, - "error": { - "type": "object" - } - }, - "required": [ - "status", - "data" - ] - }, - "DeleteScheduleOutput_2024_06_11": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" ] } }, "required": [ - "status" - ] - }, - "GetUserOutput": { - "type": "object", - "properties": { - "id": { - "type": "number", - "description": "The ID of the user", - "example": 1 - }, - "username": { - "type": "string", - "nullable": true, - "description": "The username of the user", - "example": "john_doe" - }, - "name": { - "type": "string", - "nullable": true, - "description": "The name of the user", - "example": "John Doe" - }, - "email": { - "type": "string", - "description": "The email of the user", - "example": "john@example.com" - }, - "emailVerified": { - "format": "date-time", - "type": "string", - "nullable": true, - "description": "The date when the email was verified", - "example": "2022-01-01T00:00:00Z" - }, - "bio": { - "type": "string", - "nullable": true, - "description": "The bio of the user", - "example": "I am a software developer" - }, - "avatarUrl": { - "type": "string", - "nullable": true, - "description": "The URL of the user's avatar", - "example": "https://example.com/avatar.jpg" - }, - "timeZone": { - "type": "string", - "description": "The time zone of the user", - "example": "America/New_York" - }, - "weekStart": { - "type": "string", - "description": "The week start day of the user", - "example": "Monday" - }, - "appTheme": { - "type": "string", - "nullable": true, - "description": "The app theme of the user", - "example": "light" - }, - "theme": { - "type": "string", - "nullable": true, - "description": "The theme of the user", - "example": "default" - }, - "defaultScheduleId": { - "type": "number", - "nullable": true, - "description": "The ID of the default schedule for the user", - "example": 1 - }, - "locale": { - "type": "string", - "nullable": true, - "description": "The locale of the user", - "example": "en-US" - }, - "timeFormat": { - "type": "number", - "nullable": true, - "description": "The time format of the user", - "example": 12 - }, - "hideBranding": { - "type": "boolean", - "description": "Whether to hide branding for the user", - "example": false - }, - "brandColor": { - "type": "string", - "nullable": true, - "description": "The brand color of the user", - "example": "#ffffff" - }, - "darkBrandColor": { - "type": "string", - "nullable": true, - "description": "The dark brand color of the user", - "example": "#000000" - }, - "allowDynamicBooking": { - "type": "boolean", - "nullable": true, - "description": "Whether dynamic booking is allowed for the user", - "example": true - }, - "createdDate": { - "format": "date-time", - "type": "string", - "description": "The date when the user was created", - "example": "2022-01-01T00:00:00Z" - }, - "verified": { - "type": "boolean", - "nullable": true, - "description": "Whether the user is verified", - "example": true - }, - "invitedTo": { - "type": "number", - "nullable": true, - "description": "The ID of the user who invited this user", - "example": 1 - } - }, - "required": [ - "id", - "email", - "timeZone", - "weekStart", - "hideBranding", - "createdDate" + "status", + "data" ] }, - "GetOrganizationUsersOutput": { + "GetEventTypesOutput_2024_06_14": { "type": "object", "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/GetUserOutput" + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" } } }, @@ -7210,107 +7432,194 @@ "data" ] }, - "CreateOrganizationUserInput": { + "UpdateEventTypeInput_2024_06_14": { "type": "object", "properties": { - "email": { - "type": "string", - "description": "User email address", - "example": "user@example.com" + "lengthInMinutes": { + "type": "number", + "example": 60 }, - "username": { + "title": { "type": "string", - "description": "Username", - "example": "user123" + "example": "Learn the secrets of masterchief!" }, - "weekday": { + "slug": { "type": "string", - "description": "Preferred weekday", - "example": "Monday" + "example": "learn-the-secrets-of-masterchief" }, - "brandColor": { + "description": { "type": "string", - "description": "Brand color in HEX format", - "example": "#FFFFFF" + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" }, - "darkBrandColor": { - "type": "string", - "description": "Dark brand color in HEX format", - "example": "#000000" + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } }, - "hideBranding": { + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { "type": "boolean", - "description": "Hide branding", - "example": false + "description": "If true, person booking this event't cant add guests via their emails." }, - "timeZone": { - "type": "string", - "description": "Time zone", - "example": "America/New_York" + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." }, - "theme": { - "type": "string", - "nullable": true, - "description": "Theme", - "example": "dark" + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." }, - "appTheme": { - "type": "string", - "nullable": true, - "description": "Application theme", - "example": "light" + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." }, - "timeFormat": { + "afterEventBuffer": { "type": "number", - "description": "Time format", - "example": 24 + "description": "Time spaces that can be appended after an event to give more time after it." }, - "defaultScheduleId": { + "scheduleId": { "type": "number", - "minimum": 0, - "description": "Default schedule ID", - "example": 1 + "description": "If you want that this event has different schedule than user's default one you can specify it here." }, - "locale": { - "type": "string", - "nullable": true, - "default": "en", - "description": "Locale", - "example": "en" + "bookingLimitsCount": { + "description": "Limit how many times this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + } + ] }, - "avatarUrl": { - "type": "string", - "description": "Avatar URL", - "example": "https://example.com/avatar.jpg" + "onlyShowFirstAvailableSlot": { + "type": "boolean", + "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." }, - "organizationRole": { - "type": "object", - "default": "MEMBER" + "bookingLimitsDuration": { + "description": "Limit total amount of time that this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + } + ] }, - "autoAccept": { - "type": "object", - "default": true + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "type": "number", + "description": "Offset timeslots shown to bookers by a specified number of minutes" + }, + "recurrence": { + "description": "Create a recurring event that can be booked once but will occur multiple times", + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] } }, "required": [ - "email", - "organizationRole", - "autoAccept" + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence" ] }, - "GetOrganizationUserOutput": { + "UpdateEventTypeOutput_2024_06_14": { "type": "object", "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { - "$ref": "#/components/schemas/GetUserOutput" + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" } }, "required": [ @@ -7318,46 +7627,96 @@ "data" ] }, - "UpdateOrganizationUserInput": { + "DeleteData_2024_06_14": { "type": "object", - "properties": {} + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug" + ] }, - "OrgMembershipOutputDto": { + "DeleteEventTypeOutput_2024_06_14": { "type": "object", "properties": { - "role": { + "status": { "type": "string", "enum": [ - "MEMBER", - "OWNER", - "ADMIN" - ] + "success", + "error" + ], + "example": "success" }, - "id": { - "type": "number" + "data": { + "$ref": "#/components/schemas/DeleteData_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "SelectedCalendarsInputDto": { + "type": "object", + "properties": { + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputDto": { + "type": "object", + "properties": { "userId": { "type": "number" }, - "teamId": { - "type": "number" + "integration": { + "type": "string" }, - "accepted": { - "type": "boolean" + "externalId": { + "type": "string" }, - "disableImpersonation": { - "type": "boolean" + "credentialId": { + "type": "number", + "nullable": true } }, "required": [ - "role", - "id", "userId", - "teamId", - "accepted" + "integration", + "externalId", + "credentialId" ] }, - "GetAllOrgMemberships": { + "SelectedCalendarOutputResponseDto": { "type": "object", "properties": { "status": { @@ -7369,7 +7728,7 @@ ] }, "data": { - "$ref": "#/components/schemas/OrgMembershipOutputDto" + "$ref": "#/components/schemas/SelectedCalendarOutputDto" } }, "required": [ @@ -7377,36 +7736,83 @@ "data" ] }, - "CreateOrgMembershipDto": { + "OrgTeamOutputDto": { "type": "object", "properties": { - "role": { - "type": "string", - "default": "MEMBER", - "enum": [ - "MEMBER", - "OWNER", - "ADMIN" - ] + "id": { + "type": "number" }, - "userId": { + "parentId": { "type": "number" }, - "accepted": { - "type": "boolean", - "default": false + "name": { + "type": "string", + "minLength": 1 }, - "disableImpersonation": { + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean" + }, + "isOrganization": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { "type": "boolean", "default": false + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" } }, "required": [ - "role", - "userId" + "id", + "name" ] }, - "CreateOrgMembershipOutput": { + "OrgTeamsOutputResponseDto": { "type": "object", "properties": { "status": { @@ -7418,7 +7824,10 @@ ] }, "data": { - "$ref": "#/components/schemas/OrgMembershipOutputDto" + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } } }, "required": [ @@ -7426,7 +7835,7 @@ "data" ] }, - "GetOrgMembership": { + "OrgMeTeamsOutputResponseDto": { "type": "object", "properties": { "status": { @@ -7438,7 +7847,10 @@ ] }, "data": { - "$ref": "#/components/schemas/OrgMembershipOutputDto" + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } } }, "required": [ @@ -7446,7 +7858,7 @@ "data" ] }, - "DeleteOrgMembership": { + "OrgTeamOutputResponseDto": { "type": "object", "properties": { "status": { @@ -7458,7 +7870,7 @@ ] }, "data": { - "$ref": "#/components/schemas/OrgMembershipOutputDto" + "$ref": "#/components/schemas/OrgTeamOutputDto" } }, "required": [ @@ -7466,86 +7878,276 @@ "data" ] }, - "UpdateOrgMembershipDto": { + "UpdateOrgTeamDto": { "type": "object", "properties": { - "role": { + "name": { "type": "string", - "default": "MEMBER", - "enum": [ - "MEMBER", - "OWNER", - "ADMIN" - ] + "minLength": 1 }, - "accepted": { + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { "type": "boolean", "default": false }, - "disableImpersonation": { + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + } + }, + "CreateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { "type": "boolean", "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + }, + "autoAcceptCreator": { + "type": "boolean", + "default": true + } + }, + "required": [ + "name" + ] + }, + "ScheduleAvailabilityInput_2024_06_11": { + "type": "object", + "properties": { + "days": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "example": [ + "Monday", + "Tuesday" + ], + "description": "Array of days when schedule is active." + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "08:00", + "description": "startTime must be a valid time in format HH:MM e.g. 08:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "15:00", + "description": "endTime must be a valid time in format HH:MM e.g. 15:00" } - } + }, + "required": [ + "days", + "startTime", + "endTime" + ] }, - "UpdateOrgMembership": { + "ScheduleOverrideInput_2024_06_11": { "type": "object", "properties": { - "status": { + "date": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "2024-05-20" }, - "data": { - "$ref": "#/components/schemas/OrgMembershipOutputDto" + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "12:00", + "description": "startTime must be a valid time in format HH:MM e.g. 12:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "13:00", + "description": "endTime must be a valid time in format HH:MM e.g. 13:00" } }, "required": [ - "status", - "data" + "date", + "startTime", + "endTime" ] }, - "CreateTeamEventTypeInput_2024_06_14": { + "ScheduleOutput_2024_06_11": { "type": "object", "properties": { - "lengthInMinutes": { + "id": { "type": "number", - "example": 60 + "example": 254 }, - "title": { - "type": "string", - "example": "Learn the secrets of masterchief!" + "ownerId": { + "type": "number", + "example": 478 }, - "description": { + "name": { "type": "string", - "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + "example": "Catch up hours" }, - "schedulingType": { - "type": "object" + "timeZone": { + "type": "string", + "example": "Europe/Rome" }, - "hosts": { + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "17:00", + "endTime": "19:00" + }, + { + "days": [ + "Wednesday", + "Thursday" + ], + "startTime": "16:00", + "endTime": "20:00" + } + ], "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" } }, - "assignAllTeamMembers": { - "type": "boolean" + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "18:00", + "endTime": "21:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } } }, "required": [ - "lengthInMinutes", - "title", - "description", - "schedulingType", - "hosts", - "assignAllTeamMembers" + "id", + "ownerId", + "name", + "timeZone", + "availability", + "isDefault", + "overrides" ] }, - "CreateTeamEventTypeOutput": { + "GetSchedulesOutput_2024_06_11": { "type": "object", "properties": { "status": { @@ -7557,17 +8159,13 @@ ] }, "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" - } - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "error": { + "type": "object" } }, "required": [ @@ -7575,278 +8173,187 @@ "data" ] }, - "Recurrence_2024_06_14": { - "type": "object", - "properties": { - "interval": { - "type": "number", - "example": 10, - "description": "Repeats every {count} week | month | year" - }, - "occurrences": { - "type": "number", - "example": 10, - "description": "Repeats for a maximum of {count} events" - }, - "frequency": { - "enum": [ - "yearly", - "monthly", - "weekly" - ], - "type": "string" - } - }, - "required": [ - "interval", - "occurrences", - "frequency" - ] - }, - "TeamEventTypeResponseHost": { + "CreateScheduleInput_2024_06_11": { "type": "object", "properties": { "name": { - "type": "string" - }, - "userId": { - "type": "number" - }, - "mandatory": { - "type": "boolean", - "default": false - }, - "priority": { - "type": "object", - "default": "medium" - } - }, - "required": [ - "name", - "userId" - ] - }, - "BookingLimitsCount_2024_06_14": { - "type": "object", - "properties": { - "day": { - "type": "number", - "minimum": 1, - "description": "The number of bookings per day", - "example": 1 - }, - "week": { - "type": "number", - "minimum": 1, - "description": "The number of bookings per week", - "example": 2 - }, - "month": { - "type": "number", - "minimum": 1, - "description": "The number of bookings per month", - "example": 3 - }, - "year": { - "type": "number", - "minimum": 1, - "description": "The number of bookings per year", - "example": 4 - } - } - }, - "BookingLimitsDuration_2024_06_14": { - "type": "object", - "properties": { - "day": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per day (must be a multiple of 15)", - "example": 60 - }, - "week": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per week (must be a multiple of 15)", - "example": 120 - }, - "month": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per month (must be a multiple of 15)", - "example": 180 - }, - "year": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per year (must be a multiple of 15)", - "example": 240 - } - } - }, - "TeamEventTypeOutput_2024_06_14": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1 - }, - "lengthInMinutes": { - "type": "number", - "minimum": 1 - }, - "title": { - "type": "string" - }, - "slug": { - "type": "string" + "type": "string", + "example": "Catch up hours" }, - "description": { - "type": "string" + "timeZone": { + "type": "string", + "example": "Europe/Rome", + "description": "Timezone is used to calculate available times when an event using the schedule is booked." }, - "locations": { + "availability": { + "description": "Each object contains days and times when the user is available. If not passed, the default availability is Monday to Friday from 09:00 to 17:00.", + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "17:00", + "endTime": "19:00" + }, + { + "days": [ + "Wednesday", + "Thursday" + ], + "startTime": "16:00", + "endTime": "20:00" + } + ], "type": "array", "items": { - "type": "object" + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" } }, - "bookingFields": { + "isDefault": { + "type": "boolean", + "example": true, + "description": "Each user should have 1 default schedule. If you specified `timeZone` when creating managed user, then the default schedule will be created with that timezone.\n Default schedule means that if an event type is not tied to a specific schedule then the default schedule is used." + }, + "overrides": { + "description": "Need to change availability for a specific date? Add an override.", + "example": [ + { + "date": "2024-05-20", + "startTime": "18:00", + "endTime": "21:00" + } + ], "type": "array", "items": { - "type": "object" + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" } + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, + "CreateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "disableGuests": { - "type": "boolean" - }, - "slotInterval": { - "type": "number", - "nullable": true - }, - "minimumBookingNotice": { - "type": "number", - "minimum": 0 - }, - "beforeEventBuffer": { - "type": "number" - }, - "afterEventBuffer": { - "type": "number" - }, - "schedulingType": { - "type": "object", - "nullable": true + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "recurrence": { + "data": { "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/Recurrence_2024_06_14" + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" } ] }, - "metadata": { + "error": { "type": "object" - }, - "requiresConfirmation": { - "type": "boolean" - }, - "price": { - "type": "number" - }, - "currency": { - "type": "string" - }, - "lockTimeZoneToggleOnBookingPage": { - "type": "boolean" - }, - "seatsPerTimeSlot": { - "type": "number", - "nullable": true - }, - "forwardParamsSuccessRedirect": { - "type": "boolean", - "nullable": true - }, - "successRedirectUrl": { + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { "type": "string", - "nullable": true - }, - "seatsShowAvailabilityCount": { - "type": "boolean", - "nullable": true - }, - "isInstantEvent": { - "type": "boolean" - }, - "scheduleId": { - "type": "number", - "nullable": true - }, - "teamId": { - "type": "number", - "nullable": true - }, - "ownerId": { - "type": "number", - "nullable": true + "example": "One-on-one coaching" }, - "parentEventTypeId": { - "type": "number", - "nullable": true + "timeZone": { + "type": "string", + "example": "Europe/Rome" }, - "hosts": { + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], "type": "array", "items": { - "$ref": "#/components/schemas/TeamEventTypeResponseHost" + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" } }, - "assignAllTeamMembers": { - "type": "boolean" - }, - "bookingLimitsCount": { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + "isDefault": { + "type": "boolean", + "example": true }, - "onlyShowFirstAvailableSlot": { - "type": "boolean" + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + } + }, + "UpdateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "bookingLimitsDuration": { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" }, - "bookingWindow": { + "error": { "type": "object" - }, - "offsetStart": { - "type": "number", - "minimum": 1 } }, "required": [ - "id", - "lengthInMinutes", - "title", - "slug", - "description", - "locations", - "bookingFields", - "disableGuests", - "schedulingType", - "recurrence", - "metadata", - "requiresConfirmation", - "price", - "currency", - "lockTimeZoneToggleOnBookingPage", - "seatsPerTimeSlot", - "forwardParamsSuccessRedirect", - "successRedirectUrl", - "seatsShowAvailabilityCount", - "isInstantEvent", - "scheduleId", - "hosts" + "status", + "data" ] }, - "GetTeamEventTypeOutput": { + "DeleteScheduleOutput_2024_06_11": { "type": "object", "properties": { "status": { @@ -7856,101 +8363,148 @@ "success", "error" ] - }, - "data": { - "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" } }, "required": [ - "status", - "data" + "status" ] }, - "CreatePhoneCallInput": { + "GetUserOutput": { "type": "object", "properties": { - "yourPhoneNumber": { + "id": { + "type": "number", + "description": "The ID of the user", + "example": 1 + }, + "username": { "type": "string", - "pattern": "/^\\+[1-9]\\d{1,14}$/", - "description": "Your phone number" + "nullable": true, + "description": "The username of the user", + "example": "john_doe" }, - "numberToCall": { + "name": { + "type": "string", + "nullable": true, + "description": "The name of the user", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the user", + "example": "john@example.com" + }, + "emailVerified": { + "format": "date-time", "type": "string", - "pattern": "/^\\+[1-9]\\d{1,14}$/", - "description": "Number to call" + "nullable": true, + "description": "The date when the email was verified", + "example": "2022-01-01T00:00:00Z" }, - "calApiKey": { + "bio": { "type": "string", - "description": "CAL API Key" + "nullable": true, + "description": "The bio of the user", + "example": "I am a software developer" }, - "enabled": { - "type": "object", - "default": true, - "description": "Enabled status" + "avatarUrl": { + "type": "string", + "nullable": true, + "description": "The URL of the user's avatar", + "example": "https://example.com/avatar.jpg" }, - "templateType": { - "default": "CUSTOM_TEMPLATE", - "enum": [ - "CHECK_IN_APPOINTMENT", - "CUSTOM_TEMPLATE" - ], + "timeZone": { "type": "string", - "description": "Template type" + "description": "The time zone of the user", + "example": "America/New_York" }, - "schedulerName": { + "weekStart": { "type": "string", - "description": "Scheduler name" + "description": "The week start day of the user", + "example": "Monday" }, - "guestName": { + "appTheme": { "type": "string", - "description": "Guest name" + "nullable": true, + "description": "The app theme of the user", + "example": "light" }, - "guestEmail": { + "theme": { "type": "string", - "description": "Guest email" + "nullable": true, + "description": "The theme of the user", + "example": "default" }, - "guestCompany": { + "defaultScheduleId": { + "type": "number", + "nullable": true, + "description": "The ID of the default schedule for the user", + "example": 1 + }, + "locale": { "type": "string", - "description": "Guest company" + "nullable": true, + "description": "The locale of the user", + "example": "en-US" }, - "beginMessage": { + "timeFormat": { + "type": "number", + "nullable": true, + "description": "The time format of the user", + "example": 12 + }, + "hideBranding": { + "type": "boolean", + "description": "Whether to hide branding for the user", + "example": false + }, + "brandColor": { "type": "string", - "description": "Begin message" + "nullable": true, + "description": "The brand color of the user", + "example": "#ffffff" }, - "generalPrompt": { + "darkBrandColor": { "type": "string", - "description": "General prompt" - } - }, - "required": [ - "yourPhoneNumber", - "numberToCall", - "calApiKey", - "enabled", - "templateType" - ] - }, - "CreatePhoneCallOutput": { - "type": "object", - "properties": { - "status": { + "nullable": true, + "description": "The dark brand color of the user", + "example": "#000000" + }, + "allowDynamicBooking": { + "type": "boolean", + "nullable": true, + "description": "Whether dynamic booking is allowed for the user", + "example": true + }, + "createdDate": { + "format": "date-time", "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "description": "The date when the user was created", + "example": "2022-01-01T00:00:00Z" }, - "data": { - "$ref": "#/components/schemas/Data" + "verified": { + "type": "boolean", + "nullable": true, + "description": "Whether the user is verified", + "example": true + }, + "invitedTo": { + "type": "number", + "nullable": true, + "description": "The ID of the user who invited this user", + "example": 1 } }, "required": [ - "status", - "data" + "id", + "email", + "timeZone", + "weekStart", + "hideBranding", + "createdDate" ] }, - "GetTeamEventTypesOutput": { + "GetOrganizationUsersOutput": { "type": "object", "properties": { "status": { @@ -7964,7 +8518,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + "$ref": "#/components/schemas/GetUserOutput" } } }, @@ -7973,75 +8527,95 @@ "data" ] }, - "UpdateTeamEventTypeInput_2024_06_14": { + "CreateOrganizationUserInput": { "type": "object", "properties": { - "lengthInMinutes": { - "type": "number" + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" }, - "title": { - "type": "string" + "username": { + "type": "string", + "description": "Username", + "example": "user123" }, - "slug": { - "type": "string" + "weekday": { + "type": "string", + "description": "Preferred weekday", + "example": "Monday" }, - "description": { - "type": "string" + "brandColor": { + "type": "string", + "description": "Brand color in HEX format", + "example": "#FFFFFF" }, - "locations": { - "type": "array", - "items": { - "type": "string" - } + "darkBrandColor": { + "type": "string", + "description": "Dark brand color in HEX format", + "example": "#000000" }, - "bookingFields": { - "type": "array", - "items": { - "type": "string" - } + "hideBranding": { + "type": "boolean", + "description": "Hide branding", + "example": false }, - "disableGuests": { - "type": "boolean" + "timeZone": { + "type": "string", + "description": "Time zone", + "example": "America/New_York" }, - "slotInterval": { - "type": "number" + "theme": { + "type": "string", + "nullable": true, + "description": "Theme", + "example": "dark" }, - "minimumBookingNotice": { - "type": "number" + "appTheme": { + "type": "string", + "nullable": true, + "description": "Application theme", + "example": "light" }, - "beforeEventBuffer": { - "type": "number" + "timeFormat": { + "type": "number", + "description": "Time format", + "example": 24 }, - "afterEventBuffer": { - "type": "number" + "defaultScheduleId": { + "type": "number", + "minimum": 0, + "description": "Default schedule ID", + "example": 1 }, - "hosts": { - "type": "array", - "items": { - "type": "string" - } + "locale": { + "type": "string", + "nullable": true, + "default": "en", + "description": "Locale", + "example": "en" }, - "assignAllTeamMembers": { - "type": "boolean" + "avatarUrl": { + "type": "string", + "description": "Avatar URL", + "example": "https://example.com/avatar.jpg" + }, + "organizationRole": { + "type": "object", + "default": "MEMBER" + }, + "autoAccept": { + "type": "object", + "default": true } - }, - "required": [ - "lengthInMinutes", - "title", - "slug", - "description", - "locations", - "bookingFields", - "disableGuests", - "slotInterval", - "minimumBookingNotice", - "beforeEventBuffer", - "afterEventBuffer", - "hosts", - "assignAllTeamMembers" + }, + "required": [ + "email", + "organizationRole", + "autoAccept" ] }, - "UpdateTeamEventTypeOutput": { + "GetOrganizationUserOutput": { "type": "object", "properties": { "status": { @@ -8053,17 +8627,7 @@ ] }, "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" - } - } - ] + "$ref": "#/components/schemas/GetUserOutput" } }, "required": [ @@ -8071,7 +8635,46 @@ "data" ] }, - "DeleteTeamEventTypeOutput": { + "UpdateOrganizationUserInput": { + "type": "object", + "properties": {} + }, + "OrgMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "GetAllOrgMemberships": { "type": "object", "properties": { "status": { @@ -8083,7 +8686,7 @@ ] }, "data": { - "type": "object" + "$ref": "#/components/schemas/OrgMembershipOutputDto" } }, "required": [ @@ -8091,42 +8694,36 @@ "data" ] }, - "OrgTeamMembershipOutputDto": { + "CreateOrgMembershipDto": { "type": "object", "properties": { "role": { "type": "string", + "default": "MEMBER", "enum": [ "MEMBER", "OWNER", "ADMIN" ] }, - "id": { - "type": "number" - }, "userId": { "type": "number" }, - "teamId": { - "type": "number" - }, "accepted": { - "type": "boolean" + "type": "boolean", + "default": false }, "disableImpersonation": { - "type": "boolean" + "type": "boolean", + "default": false } }, "required": [ "role", - "id", - "userId", - "teamId", - "accepted" + "userId" ] }, - "OrgTeamMembershipsOutputResponseDto": { + "CreateOrgMembershipOutput": { "type": "object", "properties": { "status": { @@ -8138,10 +8735,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" - } + "$ref": "#/components/schemas/OrgMembershipOutputDto" } }, "required": [ @@ -8149,7 +8743,7 @@ "data" ] }, - "OrgTeamMembershipOutputResponseDto": { + "GetOrgMembership": { "type": "object", "properties": { "status": { @@ -8161,7 +8755,7 @@ ] }, "data": { - "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + "$ref": "#/components/schemas/OrgMembershipOutputDto" } }, "required": [ @@ -8169,7 +8763,27 @@ "data" ] }, - "UpdateOrgTeamMembershipDto": { + "DeleteOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgMembershipDto": { "type": "object", "properties": { "role": { @@ -8191,112 +8805,218 @@ } } }, - "CreateOrgTeamMembershipDto": { + "UpdateOrgMembership": { "type": "object", "properties": { - "role": { + "status": { "type": "string", - "default": "MEMBER", + "example": "success", "enum": [ - "MEMBER", - "OWNER", - "ADMIN" + "success", + "error" ] }, - "userId": { - "type": "number" - }, - "accepted": { - "type": "boolean", - "default": false - }, - "disableImpersonation": { - "type": "boolean", - "default": false + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" } }, "required": [ - "role", - "userId" + "status", + "data" ] }, - "Attribute": { + "CreateTeamEventTypeInput_2024_06_14": { "type": "object", "properties": { - "id": { - "type": "string", - "description": "The ID of the attribute", - "example": "attr_123" - }, - "teamId": { + "lengthInMinutes": { "type": "number", - "description": "The team ID associated with the attribute", - "example": 1 + "example": 60 }, - "type": { + "title": { "type": "string", - "description": "The type of the attribute", - "enum": [ - "TEXT", - "NUMBER", - "SINGLE_SELECT", - "MULTI_SELECT" - ] + "example": "Learn the secrets of masterchief!" }, - "name": { + "slug": { "type": "string", - "description": "The name of the attribute", - "example": "Attribute Name" + "example": "learn-the-secrets-of-masterchief" }, - "slug": { + "description": { "type": "string", - "description": "The slug of the attribute", - "example": "attribute-name" + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" }, - "enabled": { + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { "type": "boolean", - "description": "Whether the attribute is enabled and displayed on their profile", - "example": true + "description": "If true, person booking this event't cant add guests via their emails." }, - "usersCanEditRelation": { + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." + }, + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." + }, + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." + }, + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." + }, + "scheduleId": { + "type": "number", + "description": "If you want that this event has different schedule than user's default one you can specify it here." + }, + "bookingLimitsCount": { + "description": "Limit how many times this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + } + ] + }, + "onlyShowFirstAvailableSlot": { "type": "boolean", - "description": "Whether users can edit the relation", - "example": true - } - }, - "required": [ - "id", - "teamId", - "type", - "name", - "slug", - "enabled" - ] - }, - "GetOrganizationAttributesOutput": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" + "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." + }, + "bookingLimitsDuration": { + "description": "Limit total amount of time that this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + } + ] + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "type": "number", + "description": "Offset timeslots shown to bookers by a specified number of minutes" + }, + "recurrence": { + "description": "Create a recurring event that can be booked once but will occur multiple times", + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } ] }, - "data": { + "schedulingType": { + "type": "object" + }, + "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Attribute" + "type": "string" } + }, + "assignAllTeamMembers": { + "type": "boolean", + "description": "If true, all current and future team members will be assigned to this event type" } }, "required": [ - "status", - "data" + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence", + "schedulingType", + "hosts", + "assignAllTeamMembers" ] }, - "GetSingleAttributeOutput": { + "CreateTeamEventTypeOutput": { "type": "object", "properties": { "status": { @@ -8308,10 +9028,15 @@ ] }, "data": { - "nullable": true, - "allOf": [ + "oneOf": [ { - "$ref": "#/components/schemas/Attribute" + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } } ] } @@ -8321,51 +9046,294 @@ "data" ] }, - "CreateOrganizationAttributeOptionInput": { + "TeamEventTypeResponseHost": { "type": "object", "properties": { - "value": { - "type": "string" + "userId": { + "type": "number", + "description": "Which user is the host of this event" }, - "slug": { - "type": "string" + "mandatory": { + "type": "boolean", + "default": false, + "description": "Only relevant for round robin event types. If true then the user must attend round robin event always." + }, + "priority": { + "type": "string", + "default": "medium", + "enum": [ + "lowest", + "low", + "medium", + "high", + "highest" + ] + }, + "name": { + "type": "string", + "example": "John Doe" } }, "required": [ - "value", - "slug" + "userId", + "name" ] }, - "CreateOrganizationAttributeInput": { + "TeamEventTypeOutput_2024_06_14": { "type": "object", "properties": { - "name": { - "type": "string" + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "minimum": 1, + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" }, "slug": { - "type": "string" + "type": "string", + "example": "learn-the-secrets-of-masterchief" }, - "type": { + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/NameDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/EmailDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/LocationDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TitleDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NotesDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/GuestsDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldOutput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "nullable": true, + "example": 60 + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0, + "example": 0 + }, + "beforeEventBuffer": { + "type": "number", + "example": 0 + }, + "afterEventBuffer": { + "type": "number", + "example": 0 + }, + "recurrence": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] + }, + "metadata": { "type": "object" }, - "options": { + "requiresConfirmation": { + "type": "boolean" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "seatsPerTimeSlot": { + "type": "number", + "nullable": true + }, + "forwardParamsSuccessRedirect": { + "type": "boolean", + "nullable": true + }, + "successRedirectUrl": { + "type": "string", + "nullable": true + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "bookingLimitsCount": { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + }, + "onlyShowFirstAvailableSlot": { + "type": "boolean" + }, + "bookingLimitsDuration": { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + }, + "bookingWindow": { "type": "array", + "description": "Limit how far in the future this event can be booked", "items": { - "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] } }, - "enabled": { + "offsetStart": { + "type": "number", + "minimum": 1 + }, + "schedulingType": { + "type": "string", + "nullable": true, + "enum": [ + "ROUND_ROBIN", + "COLLECTIVE", + "MANAGED" + ] + }, + "teamId": { + "type": "number", + "nullable": true + }, + "ownerId": { + "type": "number", + "nullable": true + }, + "parentEventTypeId": { + "type": "number", + "nullable": true, + "description": "For managed event types parent event type is the event type that this event type is based on" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeResponseHost" + } + }, + "assignAllTeamMembers": { "type": "boolean" } }, "required": [ - "name", + "id", + "lengthInMinutes", + "title", "slug", - "type", - "options" + "description", + "locations", + "bookingFields", + "disableGuests", + "recurrence", + "metadata", + "requiresConfirmation", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "seatsPerTimeSlot", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "seatsShowAvailabilityCount", + "scheduleId", + "schedulingType", + "hosts" ] }, - "CreateOrganizationAttributesOutput": { + "GetTeamEventTypeOutput": { "type": "object", "properties": { "status": { @@ -8377,7 +9345,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Attribute" + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" } }, "required": [ @@ -8385,44 +9353,86 @@ "data" ] }, - "UpdateOrganizationAttributeInput": { + "CreatePhoneCallInput": { "type": "object", "properties": { - "name": { - "type": "string" + "yourPhoneNumber": { + "type": "string", + "pattern": "/^\\+[1-9]\\d{1,14}$/", + "description": "Your phone number" }, - "slug": { - "type": "string" + "numberToCall": { + "type": "string", + "pattern": "/^\\+[1-9]\\d{1,14}$/", + "description": "Number to call" }, - "type": { - "type": "object" + "calApiKey": { + "type": "string", + "description": "CAL API Key" }, "enabled": { - "type": "boolean" + "type": "object", + "default": true, + "description": "Enabled status" + }, + "templateType": { + "default": "CUSTOM_TEMPLATE", + "enum": [ + "CHECK_IN_APPOINTMENT", + "CUSTOM_TEMPLATE" + ], + "type": "string", + "description": "Template type" + }, + "schedulerName": { + "type": "string", + "description": "Scheduler name" + }, + "guestName": { + "type": "string", + "description": "Guest name" + }, + "guestEmail": { + "type": "string", + "description": "Guest email" + }, + "guestCompany": { + "type": "string", + "description": "Guest company" + }, + "beginMessage": { + "type": "string", + "description": "Begin message" + }, + "generalPrompt": { + "type": "string", + "description": "General prompt" } - } + }, + "required": [ + "yourPhoneNumber", + "numberToCall", + "calApiKey", + "enabled", + "templateType" + ] }, - "UpdateOrganizationAttributesOutput": { + "Data": { "type": "object", "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "callId": { + "type": "string" }, - "data": { - "$ref": "#/components/schemas/Attribute" + "agentId": { + "type": "string" } }, "required": [ - "status", - "data" + "callId", + "agentId" ] }, - "DeleteOrganizationAttributesOutput": { + "CreatePhoneCallOutput": { "type": "object", "properties": { "status": { @@ -8434,7 +9444,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Attribute" + "$ref": "#/components/schemas/Data" } }, "required": [ @@ -8442,38 +9452,7 @@ "data" ] }, - "OptionOutput": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the option", - "example": "attr_option_id" - }, - "attributeId": { - "type": "string", - "description": "The ID of the attribute", - "example": "attr_id" - }, - "value": { - "type": "string", - "description": "The value of the option", - "example": "option_value" - }, - "slug": { - "type": "string", - "description": "The slug of the option", - "example": "option-slug" - } - }, - "required": [ - "id", - "attributeId", - "value", - "slug" - ] - }, - "CreateAttributeOptionOutput": { + "GetTeamEventTypesOutput": { "type": "object", "properties": { "status": { @@ -8485,7 +9464,10 @@ ] }, "data": { - "$ref": "#/components/schemas/OptionOutput" + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } } }, "required": [ @@ -8493,38 +9475,194 @@ "data" ] }, - "DeleteAttributeOptionOutput": { + "UpdateTeamEventTypeInput_2024_06_14": { "type": "object", "properties": { - "status": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean", + "description": "If true, person booking this event't cant add guests via their emails." + }, + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." + }, + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." + }, + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." + }, + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." + }, + "scheduleId": { + "type": "number", + "description": "If you want that this event has different schedule than user's default one you can specify it here." + }, + "bookingLimitsCount": { + "description": "Limit how many times this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + } ] }, - "data": { - "$ref": "#/components/schemas/OptionOutput" + "onlyShowFirstAvailableSlot": { + "type": "boolean", + "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." + }, + "bookingLimitsDuration": { + "description": "Limit total amount of time that this event can be booked", + "allOf": [ + { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + } + ] + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "type": "number", + "description": "Offset timeslots shown to bookers by a specified number of minutes" + }, + "recurrence": { + "description": "Create a recurring event that can be booked once but will occur multiple times", + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean", + "description": "If true, all current and future team members will be assigned to this event type" } }, "required": [ - "status", - "data" + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence", + "hosts", + "assignAllTeamMembers" ] }, - "UpdateOrganizationAttributeOptionInput": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "slug": { - "type": "string" - } - } - }, - "UpdateAttributeOptionOutput": { + "UpdateTeamEventTypeOutput": { "type": "object", "properties": { "status": { @@ -8536,7 +9674,17 @@ ] }, "data": { - "$ref": "#/components/schemas/OptionOutput" + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] } }, "required": [ @@ -8544,7 +9692,7 @@ "data" ] }, - "GetAllAttributeOptionOutput": { + "DeleteTeamEventTypeOutput": { "type": "object", "properties": { "status": { @@ -8556,10 +9704,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OptionOutput" - } + "type": "object" } }, "required": [ @@ -8567,46 +9712,42 @@ "data" ] }, - "AssignOrganizationAttributeOptionToUserInput": { + "OrgTeamMembershipOutputDto": { "type": "object", "properties": { - "value": { - "type": "string" - }, - "attributeOptionId": { - "type": "string" + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] }, - "attributeId": { - "type": "string" - } - }, - "required": [ - "attributeId" - ] - }, - "AssignOptionUserOutputData": { - "type": "object", - "properties": { "id": { - "type": "string", - "description": "The ID of the option assigned to the user" + "type": "number" }, - "memberId": { - "type": "number", - "description": "The ID form the org membership for the user" + "userId": { + "type": "number" }, - "attributeOptionId": { - "type": "string", - "description": "The value of the option" + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" } }, "required": [ + "role", "id", - "memberId", - "attributeOptionId" + "userId", + "teamId", + "accepted" ] }, - "AssignOptionUserOutput": { + "OrgTeamMembershipsOutputResponseDto": { "type": "object", "properties": { "status": { @@ -8618,7 +9759,10 @@ ] }, "data": { - "$ref": "#/components/schemas/AssignOptionUserOutputData" + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } } }, "required": [ @@ -8626,7 +9770,7 @@ "data" ] }, - "UnassignOptionUserOutput": { + "OrgTeamMembershipOutputResponseDto": { "type": "object", "properties": { "status": { @@ -8638,7 +9782,7 @@ ] }, "data": { - "$ref": "#/components/schemas/AssignOptionUserOutputData" + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" } }, "required": [ @@ -8646,173 +9790,111 @@ "data" ] }, - "GetOptionUserOutputData": { + "UpdateOrgTeamMembershipDto": { "type": "object", "properties": { - "id": { - "type": "string", - "description": "The ID of the option assigned to the user" - }, - "attributeId": { + "role": { "type": "string", - "description": "The ID of the attribute" + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] }, - "value": { - "type": "string", - "description": "The value of the option" + "accepted": { + "type": "boolean", + "default": false }, - "slug": { - "type": "string", - "description": "The slug of the option" + "disableImpersonation": { + "type": "boolean", + "default": false } - }, - "required": [ - "id", - "attributeId", - "value", - "slug" - ] + } }, - "GetOptionUserOutput": { + "CreateOrgTeamMembershipDto": { "type": "object", "properties": { - "status": { + "role": { "type": "string", - "example": "success", + "default": "MEMBER", "enum": [ - "success", - "error" + "MEMBER", + "OWNER", + "ADMIN" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GetOptionUserOutputData" - } - } - }, - "required": [ - "status", - "data" - ] - }, - "TeamWebhookOutputDto": { - "type": "object", - "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "teamId": { - "type": "number" - }, - "id": { + "userId": { "type": "number" }, - "triggers": { - "type": "array", - "items": { - "type": "object" - } - }, - "subscriberUrl": { - "type": "string" - }, - "active": { - "type": "boolean" - }, - "secret": { - "type": "string" - } - }, - "required": [ - "payloadTemplate", - "teamId", - "id", - "triggers", - "subscriberUrl", - "active" - ] - }, - "TeamWebhooksOutputResponseDto": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "accepted": { + "type": "boolean", + "default": false }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TeamWebhookOutputDto" - } + "disableImpersonation": { + "type": "boolean", + "default": false } }, "required": [ - "status", - "data" + "role", + "userId" ] }, - "CreateWebhookInputDto": { + "Attribute": { "type": "object", "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "triggers": { + "id": { "type": "string", - "example": [ - "BOOKING_CREATED", - "BOOKING_RESCHEDULED", - "BOOKING_CANCELLED", - "BOOKING_CONFIRMED", - "BOOKING_REJECTED", - "BOOKING_COMPLETED", - "BOOKING_NO_SHOW", - "BOOKING_REOPENED" - ], - "enum": [ - "BOOKING_CREATED", - "BOOKING_PAYMENT_INITIATED", - "BOOKING_PAID", - "BOOKING_RESCHEDULED", - "BOOKING_REQUESTED", - "BOOKING_CANCELLED", - "BOOKING_REJECTED", - "BOOKING_NO_SHOW_UPDATED", - "FORM_SUBMITTED", - "MEETING_ENDED", - "MEETING_STARTED", - "RECORDING_READY", - "INSTANT_MEETING", - "RECORDING_TRANSCRIPTION_GENERATED", - "OOO_CREATED" + "description": "The ID of the attribute", + "example": "attr_123" + }, + "teamId": { + "type": "number", + "description": "The team ID associated with the attribute", + "example": 1 + }, + "type": { + "type": "string", + "description": "The type of the attribute", + "enum": [ + "TEXT", + "NUMBER", + "SINGLE_SELECT", + "MULTI_SELECT" ] }, - "active": { - "type": "boolean" + "name": { + "type": "string", + "description": "The name of the attribute", + "example": "Attribute Name" }, - "subscriberUrl": { - "type": "string" + "slug": { + "type": "string", + "description": "The slug of the attribute", + "example": "attribute-name" }, - "secret": { - "type": "string" + "enabled": { + "type": "boolean", + "description": "Whether the attribute is enabled and displayed on their profile", + "example": true + }, + "usersCanEditRelation": { + "type": "boolean", + "description": "Whether users can edit the relation", + "example": true } }, "required": [ - "triggers", - "active", - "subscriberUrl" + "id", + "teamId", + "type", + "name", + "slug", + "enabled" ] }, - "TeamWebhookOutputResponseDto": { + "GetOrganizationAttributesOutput": { "type": "object", "properties": { "status": { @@ -8824,7 +9906,10 @@ ] }, "data": { - "$ref": "#/components/schemas/TeamWebhookOutputDto" + "type": "array", + "items": { + "$ref": "#/components/schemas/Attribute" + } } }, "required": [ @@ -8832,56 +9917,7 @@ "data" ] }, - "UpdateWebhookInputDto": { - "type": "object", - "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "triggers": { - "type": "string", - "example": [ - "BOOKING_CREATED", - "BOOKING_RESCHEDULED", - "BOOKING_CANCELLED", - "BOOKING_CONFIRMED", - "BOOKING_REJECTED", - "BOOKING_COMPLETED", - "BOOKING_NO_SHOW", - "BOOKING_REOPENED" - ], - "enum": [ - "BOOKING_CREATED", - "BOOKING_PAYMENT_INITIATED", - "BOOKING_PAID", - "BOOKING_RESCHEDULED", - "BOOKING_REQUESTED", - "BOOKING_CANCELLED", - "BOOKING_REJECTED", - "BOOKING_NO_SHOW_UPDATED", - "FORM_SUBMITTED", - "MEETING_ENDED", - "MEETING_STARTED", - "RECORDING_READY", - "INSTANT_MEETING", - "RECORDING_TRANSCRIPTION_GENERATED", - "OOO_CREATED" - ] - }, - "active": { - "type": "boolean" - }, - "subscriberUrl": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "GetDefaultScheduleOutput_2024_06_11": { + "GetSingleAttributeOutput": { "type": "object", "properties": { "status": { @@ -8893,7 +9929,12 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Attribute" + } + ] } }, "required": [ @@ -8901,219 +9942,210 @@ "data" ] }, - "CreateAvailabilityInput_2024_04_15": { + "CreateOrganizationAttributeOptionInput": { "type": "object", "properties": { - "days": { - "example": [ - 1, - 2 - ], - "type": "array", - "items": { - "type": "number" - } - }, - "startTime": { - "format": "date-time", + "value": { "type": "string" }, - "endTime": { - "format": "date-time", + "slug": { "type": "string" } }, "required": [ - "days", - "startTime", - "endTime" + "value", + "slug" ] }, - "CreateScheduleInput_2024_04_15": { + "CreateOrganizationAttributeInput": { "type": "object", "properties": { "name": { "type": "string" }, - "timeZone": { + "slug": { "type": "string" }, - "availabilities": { + "type": { + "type": "object" + }, + "options": { "type": "array", "items": { - "$ref": "#/components/schemas/CreateAvailabilityInput_2024_04_15" + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" } }, - "isDefault": { + "enabled": { "type": "boolean" } }, "required": [ "name", - "timeZone", - "isDefault" + "slug", + "type", + "options" ] }, - "WorkingHours": { + "CreateOrganizationAttributesOutput": { "type": "object", "properties": { - "days": { - "type": "array", - "items": { - "type": "number" - } - }, - "startTime": { - "type": "number" - }, - "endTime": { - "type": "number" + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "userId": { - "type": "number", - "nullable": true + "data": { + "$ref": "#/components/schemas/Attribute" } }, "required": [ - "days", - "startTime", - "endTime" + "status", + "data" ] }, - "AvailabilityModel": { + "UpdateOrganizationAttributeInput": { "type": "object", "properties": { - "id": { - "type": "number" + "name": { + "type": "string" }, - "userId": { - "type": "number", - "nullable": true + "slug": { + "type": "string" }, - "eventTypeId": { - "type": "number", - "nullable": true + "type": { + "type": "object" }, - "days": { - "type": "array", - "items": { - "type": "number" - } + "enabled": { + "type": "boolean" + } + } + }, + "UpdateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "startTime": { - "format": "date-time", - "type": "string" + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "endTime": { - "format": "date-time", - "type": "string" + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "OptionOutput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option", + "example": "attr_option_id" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_id" }, - "date": { - "format": "date-time", + "value": { "type": "string", - "nullable": true + "description": "The value of the option", + "example": "option_value" }, - "scheduleId": { - "type": "number", - "nullable": true + "slug": { + "type": "string", + "description": "The slug of the option", + "example": "option-slug" } }, "required": [ "id", - "days", - "startTime", - "endTime" + "attributeId", + "value", + "slug" ] }, - "TimeRange": { + "CreateAttributeOptionOutput": { "type": "object", "properties": { - "userId": { - "type": "number", - "nullable": true - }, - "start": { - "format": "date-time", - "type": "string" + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "end": { - "format": "date-time", - "type": "string" + "data": { + "$ref": "#/components/schemas/OptionOutput" } }, "required": [ - "start", - "end" + "status", + "data" ] }, - "ScheduleOutput": { + "DeleteAttributeOptionOutput": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "name": { - "type": "string" - }, - "isManaged": { - "type": "boolean" - }, - "workingHours": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkingHours" - } - }, - "schedule": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AvailabilityModel" - } - }, - "availability": { - "type": "array", - "items": { - "required": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/TimeRange" - } - } - }, - "timeZone": { - "type": "string" - }, - "dateOverrides": { - "type": "array", - "items": { - "type": "object" - } - }, - "isDefault": { - "type": "boolean" - }, - "isLastSchedule": { - "type": "boolean" + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "readOnly": { - "type": "boolean" + "data": { + "$ref": "#/components/schemas/OptionOutput" } }, "required": [ - "id", - "name", - "isManaged", - "workingHours", - "schedule", - "availability", - "timeZone", - "dateOverrides", - "isDefault", - "isLastSchedule", - "readOnly" + "status", + "data" ] }, - "CreateScheduleOutput_2024_04_15": { + "UpdateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "UpdateAttributeOptionOutput": { "type": "object", "properties": { "status": { @@ -9125,7 +10157,7 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" + "$ref": "#/components/schemas/OptionOutput" } }, "required": [ @@ -9133,7 +10165,7 @@ "data" ] }, - "GetDefaultScheduleOutput_2024_04_15": { + "GetAllAttributeOptionOutput": { "type": "object", "properties": { "status": { @@ -9145,12 +10177,10 @@ ] }, "data": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/ScheduleOutput" - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/OptionOutput" + } } }, "required": [ @@ -9158,7 +10188,46 @@ "data" ] }, - "GetScheduleOutput_2024_04_15": { + "AssignOrganizationAttributeOptionToUserInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "attributeOptionId": { + "type": "string" + }, + "attributeId": { + "type": "string" + } + }, + "required": [ + "attributeId" + ] + }, + "AssignOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "memberId": { + "type": "number", + "description": "The ID form the org membership for the user" + }, + "attributeOptionId": { + "type": "string", + "description": "The value of the option" + } + }, + "required": [ + "id", + "memberId", + "attributeOptionId" + ] + }, + "AssignOptionUserOutput": { "type": "object", "properties": { "status": { @@ -9170,7 +10239,7 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" + "$ref": "#/components/schemas/AssignOptionUserOutputData" } }, "required": [ @@ -9178,7 +10247,7 @@ "data" ] }, - "GetSchedulesOutput_2024_04_15": { + "UnassignOptionUserOutput": { "type": "object", "properties": { "status": { @@ -9190,7 +10259,7 @@ ] }, "data": { - "$ref": "#/components/schemas/ScheduleOutput" + "$ref": "#/components/schemas/AssignOptionUserOutputData" } }, "required": [ @@ -9198,186 +10267,173 @@ "data" ] }, - "UpdateScheduleInput_2024_04_15": { + "GetOptionUserOutputData": { "type": "object", "properties": { - "timeZone": { - "type": "string" - }, - "name": { - "type": "string" + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" }, - "isDefault": { - "type": "boolean" + "attributeId": { + "type": "string", + "description": "The ID of the attribute" }, - "schedule": { - "example": [ - [], - [ - { - "start": "2022-01-01T00:00:00.000Z", - "end": "2022-01-02T00:00:00.000Z" - } - ], - [], - [], - [], - [], - [] - ], - "items": { - "type": "array" - }, - "type": "array" + "value": { + "type": "string", + "description": "The value of the option" }, - "dateOverrides": { - "example": [ - [], - [ - { - "start": "2022-01-01T00:00:00.000Z", - "end": "2022-01-02T00:00:00.000Z" - } - ], - [], - [], - [], - [], - [] - ], - "items": { - "type": "array" - }, - "type": "array" + "slug": { + "type": "string", + "description": "The slug of the option" } }, "required": [ - "timeZone", - "name", - "isDefault", - "schedule" + "id", + "attributeId", + "value", + "slug" ] }, - "EventTypeModel_2024_04_15": { + "GetOptionUserOutput": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "eventName": { + "status": { "type": "string", - "nullable": true + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetOptionUserOutputData" + } } }, "required": [ - "id" + "status", + "data" ] }, - "AvailabilityModel_2024_04_15": { + "TeamWebhookOutputDto": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "userId": { - "type": "number", - "nullable": true + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" }, - "scheduleId": { - "type": "number", - "nullable": true + "teamId": { + "type": "number" }, - "eventTypeId": { - "type": "number", - "nullable": true + "id": { + "type": "number" }, - "days": { + "triggers": { "type": "array", "items": { - "type": "number" + "type": "object" } }, - "startTime": { - "format": "date-time", + "subscriberUrl": { "type": "string" }, - "endTime": { - "format": "date-time", + "active": { + "type": "boolean" + }, + "secret": { "type": "string" - }, - "date": { - "format": "date-time", - "type": "string", - "nullable": true } }, "required": [ + "payloadTemplate", + "teamId", "id", - "days" + "triggers", + "subscriberUrl", + "active" ] }, - "ScheduleModel_2024_04_15": { + "TeamWebhooksOutputResponseDto": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "userId": { - "type": "number" - }, - "name": { - "type": "string" - }, - "timeZone": { + "status": { "type": "string", - "nullable": true - }, - "eventType": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventTypeModel_2024_04_15" - } + "example": "success", + "enum": [ + "success", + "error" + ] }, - "availability": { + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/AvailabilityModel_2024_04_15" + "$ref": "#/components/schemas/TeamWebhookOutputDto" } } }, "required": [ - "id", - "userId", - "name" + "status", + "data" ] }, - "UpdatedScheduleOutput_2024_04_15": { + "CreateWebhookInputDto": { "type": "object", "properties": { - "schedule": { - "$ref": "#/components/schemas/ScheduleModel_2024_04_15" + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" }, - "isDefault": { + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { "type": "boolean" }, - "timeZone": { + "subscriberUrl": { "type": "string" }, - "prevDefaultId": { - "type": "number", - "nullable": true - }, - "currentDefaultId": { - "type": "number", - "nullable": true + "secret": { + "type": "string" } }, "required": [ - "schedule", - "isDefault" + "triggers", + "active", + "subscriberUrl" ] }, - "UpdateScheduleOutput_2024_04_15": { + "TeamWebhookOutputResponseDto": { "type": "object", "properties": { "status": { @@ -9389,7 +10445,7 @@ ] }, "data": { - "$ref": "#/components/schemas/UpdatedScheduleOutput_2024_04_15" + "$ref": "#/components/schemas/TeamWebhookOutputDto" } }, "required": [ @@ -9397,7 +10453,56 @@ "data" ] }, - "DeleteScheduleOutput_2024_04_15": { + "UpdateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "GetDefaultScheduleOutput_2024_06_11": { "type": "object", "properties": { "status": { @@ -9407,10 +10512,14 @@ "success", "error" ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" } }, "required": [ - "status" + "status", + "data" ] }, "AuthUrlData": { @@ -9986,287 +11095,470 @@ "properties": { "status": { "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "Attendee": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the attendee.", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the attendee.", + "example": "john.doe@example.com" + }, + "timeZone": { + "type": "string", + "description": "The time zone of the attendee.", + "example": "America/New_York" + }, + "language": { + "type": "string", + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "description": "The preferred language of the attendee. Used for booking confirmation.", + "example": "it", + "default": "en" + } + }, + "required": [ + "name", + "email", + "timeZone" + ] + }, + "CreateBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" }, - "data": { - "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputDto" + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } } }, "required": [ - "status", - "data" + "start", + "eventTypeId", + "attendee" ] }, - "Attendee": { + "CreateInstantBookingInput_2024_08_13": { "type": "object", "properties": { - "id": { - "type": "number" + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" }, - "email": { - "type": "string" + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 }, - "name": { - "type": "string" + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] }, - "timeZone": { - "type": "string" + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } }, - "locale": { + "meetingUrl": { "type": "string", - "nullable": true + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" }, - "bookingId": { - "type": "number", - "nullable": true + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + }, + "instant": { + "type": "boolean", + "description": "Flag indicating if the booking is an instant booking. Only available for team events.", + "example": true } }, "required": [ - "id", - "email", - "name", - "timeZone", - "locale", - "bookingId" + "start", + "eventTypeId", + "attendee", + "instant" ] }, - "EventType": { + "CreateRecurringBookingInput_2024_08_13": { "type": "object", "properties": { - "slug": { - "type": "string" - }, - "id": { - "type": "number" - }, - "eventName": { + "start": { "type": "string", - "nullable": true + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" }, - "price": { - "type": "number" - }, - "recurringEvent": { - "type": "object" - }, - "currency": { - "type": "string" + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 }, - "metadata": { - "type": "object" + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] }, - "seatsShowAttendees": { - "type": "object" + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } }, - "seatsShowAvailabilityCount": { - "type": "object" + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" }, - "team": { + "bookingFieldsResponses": { "type": "object", - "nullable": true + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } } }, "required": [ - "price", - "currency", - "metadata" + "start", + "eventTypeId", + "attendee" ] }, - "Reference": { + "Host": { "type": "object", "properties": { "id": { - "type": "number" - }, - "type": { - "type": "string" - }, - "uid": { - "type": "string" - }, - "meetingId": { - "type": "string", - "nullable": true - }, - "thirdPartyRecurringEventId": { - "type": "string", - "nullable": true - }, - "meetingPassword": { - "type": "string", - "nullable": true - }, - "meetingUrl": { - "type": "string", - "nullable": true - }, - "bookingId": { "type": "number", - "nullable": true + "example": 1 }, - "externalCalendarId": { + "name": { "type": "string", - "nullable": true - }, - "deleted": { - "type": "object" + "example": "Jane Doe" }, - "credentialId": { - "type": "number", - "nullable": true + "timeZone": { + "type": "string", + "example": "America/Los_Angeles" } }, "required": [ "id", - "type", - "uid", - "meetingPassword", - "bookingId", - "externalCalendarId", - "credentialId" + "name", + "timeZone" ] }, - "GetBookingsDataEntry": { + "BookingOutput_2024_08_13": { "type": "object", "properties": { "id": { - "type": "number" - }, - "title": { - "type": "string" - }, - "userPrimaryEmail": { - "type": "string", - "nullable": true + "type": "number", + "example": 123 }, - "description": { + "uid": { "type": "string", - "nullable": true - }, - "customInputs": { - "type": "object" - }, - "startTime": { - "type": "string" - }, - "endTime": { - "type": "string" + "example": "booking_uid_123" }, - "attendees": { + "hosts": { "type": "array", "items": { - "$ref": "#/components/schemas/Attendee" + "$ref": "#/components/schemas/Host" } }, - "metadata": { - "type": "object" + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending", + "rescheduled" + ], + "example": "accepted" }, - "uid": { - "type": "string" + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" }, - "recurringEventId": { + "reschedulingReason": { "type": "string", - "nullable": true + "example": "User rescheduled the event" }, - "location": { + "rescheduledFromUid": { "type": "string", - "nullable": true + "example": "previous_uid_123" }, - "eventType": { - "$ref": "#/components/schemas/EventType" + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" }, - "status": { - "type": "object" + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" }, - "paid": { - "type": "boolean" + "duration": { + "type": "number", + "example": 60 }, - "payment": { - "type": "array", - "items": { - "type": "object" - } + "eventTypeId": { + "type": "number", + "example": 45 }, - "references": { + "attendees": { "type": "array", "items": { - "$ref": "#/components/schemas/Reference" + "$ref": "#/components/schemas/Attendee" } }, - "isRecorded": { - "type": "boolean" - }, - "seatsReferences": { + "guests": { + "example": [ + "guest1@example.com", + "guest2@example.com" + ], "type": "array", "items": { - "type": "object" + "type": "string" } }, - "user": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/User" - } - ] + "meetingUrl": { + "type": "string", + "example": "https://example.com/meeting" }, - "rescheduled": { - "type": "object" + "absentHost": { + "type": "boolean", + "example": true } }, "required": [ "id", - "title", - "description", - "customInputs", - "startTime", - "endTime", - "attendees", - "metadata", "uid", - "recurringEventId", - "location", - "eventType", + "hosts", "status", - "paid", - "payment", - "references", - "isRecorded", - "seatsReferences", - "user" + "start", + "end", + "duration", + "eventTypeId", + "attendees", + "absentHost" ] }, - "GetBookingsData": { + "RecurringBookingOutput_2024_08_13": { "type": "object", "properties": { - "bookings": { + "id": { + "type": "number", + "example": 456 + }, + "uid": { + "type": "string", + "example": "recurring_uid_123" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "pending" + }, + "cancellationReason": { + "type": "string", + "example": "Event was cancelled" + }, + "reschedulingReason": { + "type": "string", + "example": "Event was rescheduled" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_recurring_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 30 + }, + "eventTypeId": { + "type": "number", + "example": 50 + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + }, + "attendees": { "type": "array", "items": { - "$ref": "#/components/schemas/GetBookingsDataEntry" + "$ref": "#/components/schemas/Attendee" } }, - "recurringInfo": { + "guests": { + "example": [ + "guest3@example.com", + "guest4@example.com" + ], "type": "array", "items": { - "type": "object" + "type": "string" } }, - "nextCursor": { - "type": "number", - "nullable": true + "meetingUrl": { + "type": "string", + "example": "https://example.com/recurring-meeting" + }, + "absentHost": { + "type": "boolean", + "example": false } }, "required": [ - "bookings", - "recurringInfo", - "nextCursor" + "id", + "uid", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "recurringBookingUid", + "attendees", + "absentHost" ] }, - "GetBookingsOutput": { + "CreateBookingOutput_2024_08_13": { "type": "object", "properties": { "status": { @@ -10278,7 +11570,18 @@ ] }, "data": { - "$ref": "#/components/schemas/GetBookingsData" + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" } }, "required": [ @@ -10286,117 +11589,41 @@ "data" ] }, - "GetBookingData": { + "GetBookingOutput_2024_08_13": { "type": "object", "properties": { - "title": { - "type": "string" - }, - "id": { - "type": "number" - }, - "uid": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "customInputs": { - "type": "object" - }, - "smsReminderNumber": { - "type": "string", - "nullable": true - }, - "recurringEventId": { - "type": "string", - "nullable": true - }, - "startTime": { - "format": "date-time", - "type": "string" - }, - "endTime": { - "format": "date-time", - "type": "string" - }, - "location": { - "type": "string", - "nullable": true - }, "status": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "cancellationReason": { - "type": "string", - "nullable": true - }, - "responses": { - "type": "object" - }, - "rejectionReason": { - "type": "string", - "nullable": true - }, - "userPrimaryEmail": { "type": "string", - "nullable": true - }, - "user": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/User" - } + "example": "success", + "enum": [ + "success", + "error" ] }, - "attendees": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Attendee" - } - }, - "eventTypeId": { - "type": "number", - "nullable": true - }, - "eventType": { - "nullable": true, - "allOf": [ + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, { - "$ref": "#/components/schemas/EventType" + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } } - ] + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" } }, "required": [ - "title", - "id", - "uid", - "description", - "customInputs", - "smsReminderNumber", - "recurringEventId", - "startTime", - "endTime", - "location", "status", - "metadata", - "cancellationReason", - "responses", - "rejectionReason", - "userPrimaryEmail", - "user", - "attendees", - "eventTypeId", - "eventType" + "data" ] }, - "GetBookingOutput": { + "GetBookingsOutput_2024_08_13": { "type": "object", "properties": { "status": { @@ -10408,7 +11635,18 @@ ] }, "data": { - "$ref": "#/components/schemas/GetBookingData" + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ] + }, + "description": "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects" } }, "required": [ @@ -10416,162 +11654,125 @@ "data" ] }, - "Response": { + "RescheduleBookingInput_2024_08_13": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string" - }, - "guests": { - "type": "array", - "items": { - "type": "string" - } - }, - "location": { - "$ref": "#/components/schemas/Location" + "start": { + "type": "string", + "description": "Start time in ISO 8601 format for the new booking", + "example": "2024-08-13T10:00:00Z" }, - "notes": { - "type": "string" + "reschedulingReason": { + "type": "string", + "example": "User requested reschedule", + "description": "Reason for rescheduling the booking" } }, "required": [ - "name", - "email", - "guests" + "start" ] }, - "CreateBookingInput": { + "RescheduleBookingOutput_2024_08_13": { "type": "object", "properties": { - "end": { - "type": "string" - }, - "start": { - "type": "string" - }, - "eventTypeId": { - "type": "number" - }, - "eventTypeSlug": { - "type": "string" - }, - "rescheduleUid": { - "type": "string" - }, - "timeZone": { - "type": "string" - }, - "user": { - "type": "array", - "items": { - "type": "string" - } - }, - "language": { - "type": "string" - }, - "bookingUid": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "hasHashedBookingLink": { - "type": "boolean" - }, - "hashedLink": { + "status": { "type": "string", - "nullable": true - }, - "seatReferenceUid": { - "type": "string" - }, - "responses": { - "$ref": "#/components/schemas/Response" - }, - "orgSlug": { - "type": "string" + "example": "success", + "enum": [ + "success", + "error" + ] }, - "locationUrl": { - "type": "string" + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" } }, "required": [ - "start", - "eventTypeId", - "timeZone", - "language", - "metadata", - "hashedLink", - "responses" + "status", + "data" ] }, - "CancelBookingInput": { + "CancelBookingInput_2024_08_13": { "type": "object", "properties": { - "id": { - "type": "number" - }, - "uid": { - "type": "string" - }, - "allRemainingBookings": { - "type": "boolean" - }, "cancellationReason": { - "type": "string" - }, - "seatReferenceUid": { - "type": "string" + "type": "string", + "example": "User requested cancellation" } }, "required": [ - "id", - "uid", - "allRemainingBookings", - "cancellationReason", - "seatReferenceUid" + "cancellationReason" ] }, - "MarkNoShowInput": { + "CancelBookingOutput_2024_08_13": { "type": "object", "properties": { - "noShowHost": { - "type": "boolean" + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] }, - "attendees": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Attendee" - } + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" } - } + }, + "required": [ + "status", + "data" + ] }, - "HandleMarkNoShowData": { + "MarkAbsentBookingInput_2024_08_13": { "type": "object", "properties": { - "message": { - "type": "string" - }, - "noShowHost": { - "type": "boolean" + "host": { + "type": "boolean", + "example": false, + "description": "Whether the host was absent" }, "attendees": { + "description": "Toggle whether an attendee was absent or not.", + "example": [ + { + "absent": true, + "email": "someone@gmail.com" + } + ], "type": "array", "items": { - "$ref": "#/components/schemas/Attendee" + "type": "string" } } }, "required": [ - "message" + "attendees" ] }, - "MarkNoShowOutput": { + "MarkAbsentBookingOutput_2024_08_13": { "type": "object", "properties": { "status": { @@ -10583,7 +11784,15 @@ ] }, "data": { - "$ref": "#/components/schemas/HandleMarkNoShowData" + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" } }, "required": [ diff --git a/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts index 83b7e5f21a6664..5bc6ccacb554da 100644 --- a/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts @@ -3,6 +3,8 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { TestingModule } from "@nestjs/testing"; import { Booking, User } from "@prisma/client"; +import { Prisma } from "@calcom/prisma/client"; + export class BookingsRepositoryFixture { private prismaReadClient: PrismaReadService["prisma"]; private prismaWriteClient: PrismaWriteService["prisma"]; @@ -16,6 +18,14 @@ export class BookingsRepositoryFixture { return this.prismaReadClient.booking.findFirst({ where: { id: bookingId } }); } + async getByUid(bookingUid: Booking["uid"]) { + return this.prismaReadClient.booking.findUnique({ where: { uid: bookingUid } }); + } + + async create(booking: Prisma.BookingCreateInput) { + return this.prismaWriteClient.booking.create({ data: booking }); + } + async deleteById(bookingId: Booking["id"]) { return this.prismaWriteClient.booking.delete({ where: { id: bookingId } }); } diff --git a/apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts new file mode 100644 index 00000000000000..304fdf1415a690 --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts @@ -0,0 +1,25 @@ +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; +import { EventType } from "@prisma/client"; + +import { Prisma } from "@calcom/prisma/client"; + +export class HostsRepositoryFixture { + private prismaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.prismaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async create(data: Prisma.HostCreateInput) { + return this.prismaWriteClient.host.create({ + data: { + ...data, + }, + }); + } +} diff --git a/apps/api/v2/test/setEnvVars.ts b/apps/api/v2/test/setEnvVars.ts index e7a2393878329e..102a16fc138dd6 100644 --- a/apps/api/v2/test/setEnvVars.ts +++ b/apps/api/v2/test/setEnvVars.ts @@ -22,4 +22,7 @@ const env: Partial> = { process.env = { ...env, ...process.env, + NEXT_PUBLIC_VAPID_PUBLIC_KEY: + "BIds0AQJ96xGBjTSMHTOqLBLutQE7Lu32KKdgSdy7A2cS4mKI2cgb3iGkhDJa5Siy-stezyuPm8qpbhmNxdNHMw", + VAPID_PRIVATE_KEY: "6cJtkASCar5sZWguIAW7OjvyixpBw9p8zL8WDDwk9Jk", }; diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md index bfb263520b62ae..aff4d76b1e669f 100644 --- a/apps/web/CHANGELOG.md +++ b/apps/web/CHANGELOG.md @@ -1,5 +1,14 @@ # @calcom/web +## 4.5.2 + +### Patch Changes + +- Updated dependencies + - @calcom/embed-core@1.5.1 + - @calcom/embed-react@1.5.1 + - @calcom/embed-snippet@1.3.1 + ## 4.0.8 ### Patch Changes diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index 50e5e3d2aae937..c04f08fd15297e 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -28,7 +28,9 @@ const ROUTES: [URLPattern, boolean][] = [ ["/getting-started/:step", process.env.APP_ROUTER_GETTING_STARTED_STEP_ENABLED === "1"] as const, ["/apps", process.env.APP_ROUTER_APPS_ENABLED === "1"] as const, ["/bookings/:status", process.env.APP_ROUTER_BOOKINGS_STATUS_ENABLED === "1"] as const, + ["/booking/:path*", process.env.APP_ROUTER_BOOKING_ENABLED === "1"] as const, ["/video/:path*", process.env.APP_ROUTER_VIDEO_ENABLED === "1"] as const, + ["/team", process.env.APP_ROUTER_TEAM_ENABLED === "1"] as const, ["/teams", process.env.APP_ROUTER_TEAMS_ENABLED === "1"] as const, ["/more", process.env.APP_ROUTER_MORE_ENABLED === "1"] as const, ["/maintenance", process.env.APP_ROUTER_MAINTENANCE_ENABLED === "1"] as const, diff --git a/apps/web/app/_trpc/trpc-provider.tsx b/apps/web/app/_trpc/trpc-provider.tsx index 45100bd89f4c3f..6bb9e77a7185b2 100644 --- a/apps/web/app/_trpc/trpc-provider.tsx +++ b/apps/web/app/_trpc/trpc-provider.tsx @@ -55,7 +55,9 @@ export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState // adds pretty logs to your console in development and logs errors in production loggerLink({ enabled: (opts) => - !!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error), + (typeof process.env.NEXT_PUBLIC_LOGGER_LEVEL === "number" && + process.env.NEXT_PUBLIC_LOGGER_LEVEL >= 0) || + (opts.direction === "down" && opts.result instanceof Error), }), splitLink({ // check for context property `skipBatch` diff --git a/apps/web/app/future/apps/[slug]/[...pages]/page.tsx b/apps/web/app/future/apps/[slug]/[...pages]/page.tsx index ad854995bdebfe..e67645d9fb8cf2 100644 --- a/apps/web/app/future/apps/[slug]/[...pages]/page.tsx +++ b/apps/web/app/future/apps/[slug]/[...pages]/page.tsx @@ -1,9 +1,8 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { SearchParams } from "app/_types"; +import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import type { GetServerSidePropsResult } from "next"; -import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; import { cookies, headers } from "next/headers"; import { notFound } from "next/navigation"; import z from "zod"; @@ -18,13 +17,7 @@ const paramsSchema = z.object({ pages: z.array(z.string()), }); -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +export const generateMetadata = async ({ params, searchParams }: PageProps) => { const p = paramsSchema.safeParse(params); if (!p.success) { @@ -34,7 +27,7 @@ export const generateMetadata = async ({ const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams); const data = await getData(legacyContext); const form = "form" in data ? (data.form as { name?: string; description?: string }) : null; - const formName = form?.name ?? "Forms"; + const formName = form?.name ?? "Routing Forms"; const formDescription = form?.description ?? ""; return await _generateMetadata( diff --git a/apps/web/app/future/auth/setup/page.tsx b/apps/web/app/future/auth/setup/page.tsx index b33b9693ab55dc..5f4d478e455a63 100644 --- a/apps/web/app/future/auth/setup/page.tsx +++ b/apps/web/app/future/auth/setup/page.tsx @@ -1,12 +1,13 @@ -import Setup from "@pages/auth/setup"; import { withAppDirSsr } from "app/WithAppDirSsr"; import { WithLayout } from "app/layoutHOC"; -import type { InferGetServerSidePropsType } from "next"; import { getServerSideProps } from "@server/lib/setup/getServerSideProps"; +import Setup from "~/auth/setup-view"; +import type { PageProps } from "~/auth/setup-view"; + export default WithLayout({ getLayout: null, Page: Setup, - getData: withAppDirSsr>(getServerSideProps), + getData: withAppDirSsr(getServerSideProps), })<"P">; diff --git a/apps/web/app/future/auth/signin/page.tsx b/apps/web/app/future/auth/signin/page.tsx index bb19d2bc210d17..18354c4f9c7b9e 100644 --- a/apps/web/app/future/auth/signin/page.tsx +++ b/apps/web/app/future/auth/signin/page.tsx @@ -1,13 +1,14 @@ -import signin from "@pages/auth/signin"; import { withAppDirSsr } from "app/WithAppDirSsr"; import { WithLayout } from "app/layoutHOC"; -import type { InferGetServerSidePropsType } from "next"; import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps"; +import SignIn from "~/auth/signin-view"; +import type { PageProps } from "~/auth/signin-view"; + export default WithLayout({ getLayout: null, - Page: signin, + Page: SignIn, // @ts-expect-error TODO: fix this - getData: withAppDirSsr>(getServerSideProps), + getData: withAppDirSsr(getServerSideProps), })<"P">; diff --git a/apps/web/app/future/auth/sso/[provider]/page.tsx b/apps/web/app/future/auth/sso/[provider]/page.tsx index 2a59c87a8ac1c5..efac8c1f18d3a8 100644 --- a/apps/web/app/future/auth/sso/[provider]/page.tsx +++ b/apps/web/app/future/auth/sso/[provider]/page.tsx @@ -1,12 +1,13 @@ -import Provider from "@pages/auth/sso/[provider]"; import { withAppDirSsr } from "app/WithAppDirSsr"; import { WithLayout } from "app/layoutHOC"; -import type { InferGetServerSidePropsType } from "next"; import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps"; +import type { SSOProviderPageProps } from "~/auth/sso/provider-view"; +import SSOProviderView from "~/auth/sso/provider-view"; + export default WithLayout({ getLayout: null, - Page: Provider, - getData: withAppDirSsr>(getServerSideProps), + Page: SSOProviderView, + getData: withAppDirSsr(getServerSideProps), })<"P">; diff --git a/apps/web/app/future/auth/sso/direct/page.tsx b/apps/web/app/future/auth/sso/direct/page.tsx index 0f69f324feaf8a..a920c02ba34f40 100644 --- a/apps/web/app/future/auth/sso/direct/page.tsx +++ b/apps/web/app/future/auth/sso/direct/page.tsx @@ -1,11 +1,13 @@ -import DirectSSOLogin from "@pages/auth/sso/direct"; import { withAppDirSsr } from "app/WithAppDirSsr"; import { WithLayout } from "app/layoutHOC"; import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps"; +import type { SSODirectPageProps } from "~/auth/sso/direct-view"; +import SSODirectView from "~/auth/sso/direct-view"; + export default WithLayout({ getLayout: null, - Page: DirectSSOLogin, - getData: withAppDirSsr(getServerSideProps), + Page: SSODirectView, + getData: withAppDirSsr(getServerSideProps), })<"P">; diff --git a/apps/web/app/future/booking/[uid]/page.tsx b/apps/web/app/future/booking/[uid]/page.tsx index 238dd365eea6c5..e192d88b61211c 100644 --- a/apps/web/app/future/booking/[uid]/page.tsx +++ b/apps/web/app/future/booking/[uid]/page.tsx @@ -1,5 +1,5 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { Params, SearchParams } from "app/_types"; +import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -11,13 +11,7 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import OldPage from "~/bookings/views/bookings-single-view"; import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps"; -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const { bookingInfo, eventType, recurringBookings } = await getData( buildLegacyCtx(headers(), cookies(), params, searchParams) ); diff --git a/apps/web/app/future/bookings/[status]/page.tsx b/apps/web/app/future/bookings/[status]/page.tsx index 15f5630fa0f11e..a4a5a0e058a85c 100644 --- a/apps/web/app/future/bookings/[status]/page.tsx +++ b/apps/web/app/future/bookings/[status]/page.tsx @@ -3,8 +3,6 @@ import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import type { InferGetStaticPropsType } from "next"; -import { APP_NAME } from "@calcom/lib/constants"; - import { validStatuses } from "~/bookings/lib/validStatuses"; import Page from "~/bookings/views/bookings-listing-view"; import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps"; @@ -14,8 +12,8 @@ const getData = withAppDirSsg(getStaticProps); export const generateMetadata = async () => await _generateMetadata( - (t) => `${APP_NAME} | ${t("bookings")}`, - () => "" + () => "Bookings", + () => "Create events to share for people to book on your calendar." ); export const generateStaticParams = async () => { diff --git a/apps/web/app/future/d/[link]/[slug]/page.tsx b/apps/web/app/future/d/[link]/[slug]/page.tsx index 03c250363f5953..0d194c685322de 100644 --- a/apps/web/app/future/d/[link]/[slug]/page.tsx +++ b/apps/web/app/future/d/[link]/[slug]/page.tsx @@ -1,6 +1,5 @@ -import LegacyPage from "@pages/d/[link]/[slug]"; import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { Params, SearchParams } from "app/_types"; +import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -10,14 +9,11 @@ import { EventRepository } from "@calcom/lib/server/repository/event"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps"; +import { type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps"; -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +import Type from "~/d/[link]/d-type-view"; + +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); const pageProps = await getData(legacyCtx); @@ -42,5 +38,5 @@ export const generateMetadata = async ({ ); }; -const getData = withAppDirSsr(getServerSideProps); -export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">; +const getData = withAppDirSsr(getServerSideProps); +export default WithLayout({ getLayout: null, Page: Type, getData })<"P">; diff --git a/apps/web/app/future/event-types/[type]/page.tsx b/apps/web/app/future/event-types/[type]/page.tsx index 6cc40aa59ce0e5..6bd794a2b1aac0 100644 --- a/apps/web/app/future/event-types/[type]/page.tsx +++ b/apps/web/app/future/event-types/[type]/page.tsx @@ -4,12 +4,12 @@ import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; +import { EventType } from "@calcom/atoms/monorepo"; + import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps"; import type { PageProps as EventTypePageProps } from "@lib/event-types/[type]/getServerSideProps"; -import EventTypePageWrapper from "~/event-types/views/event-types-single-view"; - export const generateMetadata = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); const { eventType } = await getData(legacyCtx); @@ -21,5 +21,5 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => { }; const getData = withAppDirSsr(getServerSideProps); -const Page = (props: EventTypePageProps) => ; +const Page = ({ type, ...rest }: EventTypePageProps) => ; export default WithLayout({ getLayout: null, getData, Page })<"P">; diff --git a/apps/web/app/future/insights/page.tsx b/apps/web/app/future/insights/page.tsx index 24f94e4f8e5da3..1260490c07815b 100644 --- a/apps/web/app/future/insights/page.tsx +++ b/apps/web/app/future/insights/page.tsx @@ -1,10 +1,11 @@ -import LegacyPage from "@pages/insights/index"; import { withAppDirSsr } from "app/WithAppDirSsr"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { getServerSideProps } from "@lib/insights/getServerSideProps"; -import { type inferSSRProps } from "@lib/types/inferSSRProps"; + +import type { PageProps } from "~/insights/insights-view"; +import InsightsPage from "~/insights/insights-view"; export const generateMetadata = async () => await _generateMetadata( @@ -12,6 +13,6 @@ export const generateMetadata = async () => (t) => t("insights_subtitle") ); -const getData = withAppDirSsr>(getServerSideProps); +const getData = withAppDirSsr(getServerSideProps); -export default WithLayout({ getLayout: null, getData, Page: LegacyPage }); +export default WithLayout({ getLayout: null, getData, Page: InsightsPage }); diff --git a/apps/web/app/future/org/[orgSlug]/embed/page.tsx b/apps/web/app/future/org/[orgSlug]/embed/page.tsx index 499173cf980066..111865eeaf5403 100644 --- a/apps/web/app/future/org/[orgSlug]/embed/page.tsx +++ b/apps/web/app/future/org/[orgSlug]/embed/page.tsx @@ -1,6 +1,6 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; import withEmbedSsrAppDir from "app/WithEmbedSSR"; -import type { Params, SearchParams } from "app/_types"; +import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -13,13 +13,7 @@ import TeamPage from "~/team/team-view"; const getData = withAppDirSsr(getServerSideProps); -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); const teamName = props.team.name || "Nameless Team"; diff --git a/apps/web/app/future/org/[orgSlug]/page.tsx b/apps/web/app/future/org/[orgSlug]/page.tsx index d6572bbb918fbd..b84f78b9679312 100644 --- a/apps/web/app/future/org/[orgSlug]/page.tsx +++ b/apps/web/app/future/org/[orgSlug]/page.tsx @@ -1,5 +1,5 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { Params, SearchParams } from "app/_types"; +import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -10,13 +10,7 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; import type { PageProps } from "~/team/team-view"; import TeamPage from "~/team/team-view"; -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); const teamName = props.team.name || "Nameless Team"; diff --git a/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx b/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx index 22cb127b721915..ea51f5cd9b771b 100644 --- a/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx +++ b/apps/web/app/future/org/[orgSlug]/team/[slug]/[type]/page.tsx @@ -1,5 +1,5 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { Params, SearchParams } from "app/_types"; +import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -13,13 +13,7 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps"; import type { PageProps } from "~/team/type-view"; import TypePage from "~/team/type-view"; -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); const props = await getData(legacyCtx); const { user: username, slug: eventSlug, booking } = props; diff --git a/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx b/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx index d6572bbb918fbd..b84f78b9679312 100644 --- a/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx +++ b/apps/web/app/future/org/[orgSlug]/team/[slug]/page.tsx @@ -1,5 +1,5 @@ import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { Params, SearchParams } from "app/_types"; +import type { PageProps as _PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; @@ -10,13 +10,7 @@ import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps"; import type { PageProps } from "~/team/team-view"; import TeamPage from "~/team/team-view"; -export const generateMetadata = async ({ - params, - searchParams, -}: { - params: Params; - searchParams: SearchParams; -}) => { +export const generateMetadata = async ({ params, searchParams }: _PageProps) => { const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams)); const teamName = props.team.name || "Nameless Team"; diff --git a/apps/web/app/future/page.tsx b/apps/web/app/future/page.tsx new file mode 100644 index 00000000000000..f82ff90851cf9c --- /dev/null +++ b/apps/web/app/future/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from "next/navigation"; + +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; + +const RedirectPage = async () => { + const session = await getServerSessionForAppDir(); + if (!session?.user?.id) { + redirect("/auth/login"); + } + redirect("/event-types"); +}; + +export default RedirectPage; diff --git a/apps/web/app/future/reschedule/[uid]/embed/page.tsx b/apps/web/app/future/reschedule/[uid]/embed/page.tsx index 4cc22aabf2903f..52f7f7e1d93e77 100644 --- a/apps/web/app/future/reschedule/[uid]/embed/page.tsx +++ b/apps/web/app/future/reschedule/[uid]/embed/page.tsx @@ -1,14 +1,15 @@ -import { getServerSideProps } from "@pages/reschedule/[uid]"; -import { withAppDirSsr } from "app/WithAppDirSsr"; +import { getServerSideProps as _getServerSideProps } from "@pages/reschedule/[uid]"; import type { PageProps } from "app/_types"; import { cookies, headers } from "next/headers"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import withEmbedSsr from "@lib/withEmbedSsr"; +const getData = withEmbedSsr(_getServerSideProps); + const Page = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); - await withAppDirSsr(withEmbedSsr(getServerSideProps))(legacyCtx); + await getData(legacyCtx); return null; }; diff --git a/apps/web/app/future/reschedule/[uid]/page.tsx b/apps/web/app/future/reschedule/[uid]/page.tsx index 921369891f3e1f..4f629a3ba0fd70 100644 --- a/apps/web/app/future/reschedule/[uid]/page.tsx +++ b/apps/web/app/future/reschedule/[uid]/page.tsx @@ -1,10 +1,10 @@ -import { getServerSideProps } from "@pages/reschedule/[uid]"; import { withAppDirSsr } from "app/WithAppDirSsr"; import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { headers, cookies } from "next/headers"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; +import { getServerSideProps } from "@lib/reschedule/[uid]/getServerSideProps"; export const generateMetadata = async () => await _generateMetadata( diff --git a/apps/web/app/future/routing-forms/[...pages]/page.tsx b/apps/web/app/future/routing-forms/[...pages]/page.tsx index 3ee4fbd90d2242..4ecc5a1aa0839f 100644 --- a/apps/web/app/future/routing-forms/[...pages]/page.tsx +++ b/apps/web/app/future/routing-forms/[...pages]/page.tsx @@ -1,12 +1,19 @@ +import type { PageProps } from "app/_types"; import { redirect } from "next/navigation"; +import z from "zod"; -const getPageProps = () => { - return redirect(`/apps/routing-forms/forms`); -}; -const Page = () => { - getPageProps(); +const paramsSchema = z + .object({ + pages: z.array(z.string()), + }) + .catch({ + pages: [], + }); + +const Page = ({ params, searchParams }: PageProps) => { + const { pages } = paramsSchema.parse({ ...params, ...searchParams }); - return null; + redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`); }; export default Page; diff --git a/apps/web/app/future/routing-forms/page.tsx b/apps/web/app/future/routing-forms/page.tsx index f904f3eaf4b063..ba222ba47617af 100644 --- a/apps/web/app/future/routing-forms/page.tsx +++ b/apps/web/app/future/routing-forms/page.tsx @@ -1,30 +1,7 @@ -import type { PageProps } from "app/_types"; -import { type GetServerSidePropsContext } from "next"; -import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; -import z from "zod"; -import { buildLegacyCtx } from "@lib/buildLegacyCtx"; - -const paramsSchema = z - .object({ - pages: z.array(z.string()), - }) - .catch({ - pages: [], - }); - -const getPageProps = async (context: GetServerSidePropsContext) => { - const { pages } = paramsSchema.parse(context.params); - - return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`); -}; - -const Page = async ({ params, searchParams }: PageProps) => { - const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); - await getPageProps(legacyCtx); - - return null; +const Page = () => { + redirect("/apps/routing-forms/forms"); }; export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/page.tsx b/apps/web/app/future/settings/(admin)/admin/page.tsx index 77f09f99a3dba7..4efacc89bb0189 100644 --- a/apps/web/app/future/settings/(admin)/admin/page.tsx +++ b/apps/web/app/future/settings/(admin)/admin/page.tsx @@ -1,8 +1,4 @@ -import LegacyPage from "@pages/settings/admin/index"; import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir"; export const generateMetadata = async () => await _generateMetadata( @@ -10,4 +6,5 @@ export const generateMetadata = async () => () => "admin_description" ); -export default WithLayout({ getServerLayout: getLayout, Page: LegacyPage })<"P">; +const Page = () =>

Admin index

; +export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/billing/page.tsx b/apps/web/app/future/settings/(settings)/organizations/billing/page.tsx index 52080e55f5b6c8..7993b194a42783 100644 --- a/apps/web/app/future/settings/(settings)/organizations/billing/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/billing/page.tsx @@ -1,11 +1,5 @@ -import { _generateMetadata } from "app/_utils"; +import BillingPage, { generateMetadata } from "../../billing/page"; -import Page from "~/settings/billing/billing-view"; +export { generateMetadata }; -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("billing"), - (t) => t("manage_billing_description") - ); - -export default Page; +export default BillingPage; diff --git a/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/general/page.tsx b/apps/web/app/future/settings/(settings)/organizations/general/page.tsx index e55435094bf079..30416c6daa3c08 100644 --- a/apps/web/app/future/settings/(settings)/organizations/general/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/general/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/organizations/pages/settings/general"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,15 @@ export const generateMetadata = async () => (t) => t("general_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/members/page.tsx b/apps/web/app/future/settings/(settings)/organizations/members/page.tsx index 5c90c6869eac7c..e4ee4b40d1400b 100644 --- a/apps/web/app/future/settings/(settings)/organizations/members/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/members/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/organizations/pages/settings/members"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/organizations/pages/settings/members"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,15 @@ export const generateMetadata = async () => (t) => t("organization_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/profile/page.tsx b/apps/web/app/future/settings/(settings)/organizations/profile/page.tsx index 3450c5abddde9b..a9e54e098b3c53 100644 --- a/apps/web/app/future/settings/(settings)/organizations/profile/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/profile/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/organizations/pages/settings/profile"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,18 @@ export const generateMetadata = async () => (t) => t("profile_org_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/sso/page.tsx b/apps/web/app/future/settings/(settings)/organizations/sso/page.tsx new file mode 100644 index 00000000000000..55c561e75baf31 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/sso/page.tsx @@ -0,0 +1,24 @@ +import { _generateMetadata, getFixedT } from "app/_utils"; + +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("sso_configuration"), + (t) => t("sso_configuration_description_orgs") + ); + +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/page.tsx index ac76104d07a5f0..25f31d3027b6d4 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/teams/pages/team-appearance-view"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,18 @@ export const generateMetadata = async () => (t) => t("appearance_team_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/page.tsx index 9e5018ada3668e..1896558bec3d29 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/organizations/pages/settings/other-team-members-view"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-members-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,16 @@ export const generateMetadata = async () => (t) => t("members_team_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + // TODO: Add CTA Button + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/page.tsx index 66cf714fbcc5b5..11a5626af0b206 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,15 @@ export const generateMetadata = async () => (t) => t("profile_team_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx deleted file mode 100644 index 9da17cb2818d7b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; - -export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/page.ts b/apps/web/app/future/settings/(settings)/organizations/teams/other/page.ts deleted file mode 100644 index ffaf5302cb166b..00000000000000 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/page.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { _generateMetadata } from "app/_utils"; - -import Page from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("org_admin_other_teams"), - (t) => t("org_admin_other_teams_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/page.tsx new file mode 100644 index 00000000000000..b68b2199cd27cf --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/page.tsx @@ -0,0 +1,24 @@ +import { _generateMetadata, getFixedT } from "app/_utils"; + +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("org_admin_other_teams"), + (t) => t("org_admin_other_teams_description") + ); + +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/teams/[id]/appearance/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/appearance/page.tsx index ac76104d07a5f0..25f31d3027b6d4 100644 --- a/apps/web/app/future/settings/(settings)/teams/[id]/appearance/page.tsx +++ b/apps/web/app/future/settings/(settings)/teams/[id]/appearance/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/teams/pages/team-appearance-view"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/teams/pages/team-appearance-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,18 @@ export const generateMetadata = async () => (t) => t("appearance_team_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx index 34e7f004a4d0a0..e88787f0b75dd1 100644 --- a/apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx +++ b/apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx @@ -1,11 +1,4 @@ -import { _generateMetadata } from "app/_utils"; +import BillingPage, { generateMetadata } from "../../../billing/page"; -import Page from "~/settings/billing/billing-view"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("billing"), - (t) => t("team_billing_description") - ); - -export default Page; +export { generateMetadata }; +export default BillingPage; diff --git a/apps/web/app/future/settings/(settings)/teams/[id]/members/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/members/page.tsx index 0f38c54f5975d2..711a118776de23 100644 --- a/apps/web/app/future/settings/(settings)/teams/[id]/members/page.tsx +++ b/apps/web/app/future/settings/(settings)/teams/[id]/members/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/teams/pages/team-members-view"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/teams/pages/team-members-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,15 @@ export const generateMetadata = async () => (t) => t("members_team_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/teams/[id]/profile/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/profile/page.tsx index b2e02352ef39f2..b08a143563289d 100644 --- a/apps/web/app/future/settings/(settings)/teams/[id]/profile/page.tsx +++ b/apps/web/app/future/settings/(settings)/teams/[id]/profile/page.tsx @@ -1,6 +1,8 @@ -import { _generateMetadata } from "app/_utils"; +import { _generateMetadata, getFixedT } from "app/_utils"; -import Page from "@calcom/features/ee/teams/pages/team-profile-view"; +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/teams/pages/team-profile-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; export const generateMetadata = async () => await _generateMetadata( @@ -8,4 +10,18 @@ export const generateMetadata = async () => (t) => t("profile_team_description") ); +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + export default Page; diff --git a/apps/web/app/future/settings/(settings)/teams/page.ts b/apps/web/app/future/settings/(settings)/teams/page.ts deleted file mode 100644 index 6175f853ecda7e..00000000000000 --- a/apps/web/app/future/settings/(settings)/teams/page.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { _generateMetadata } from "app/_utils"; - -import Page from "@calcom/features/ee/teams/pages/team-listing-view"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("teams"), - (t) => t("create_manage_teams_collaborative") - ); - -export default Page; diff --git a/apps/web/app/future/settings/(settings)/teams/page.tsx b/apps/web/app/future/settings/(settings)/teams/page.tsx new file mode 100644 index 00000000000000..6aa5cd10734c4a --- /dev/null +++ b/apps/web/app/future/settings/(settings)/teams/page.tsx @@ -0,0 +1,24 @@ +import { _generateMetadata, getFixedT } from "app/_utils"; + +import { getServerSessionForAppDir } from "@calcom/features/auth/lib/get-server-session-for-app-dir"; +import LegacyPage from "@calcom/features/ee/teams/pages/team-listing-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("teams"), + (t) => t("create_manage_teams_collaborative") + ); + +const Page = async () => { + const session = await getServerSessionForAppDir(); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/organizations/[id]/about/page.tsx b/apps/web/app/future/settings/organizations/[id]/about/page.tsx index 42fca898e47d46..cb899d1e0b6a62 100644 --- a/apps/web/app/future/settings/organizations/[id]/about/page.tsx +++ b/apps/web/app/future/settings/organizations/[id]/about/page.tsx @@ -1,6 +1,9 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization"; + import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/about-view"; export const generateMetadata = async () => @@ -9,7 +12,10 @@ export const generateMetadata = async () => (t) => t("about_your_organization_description") ); +const getData = withAppDirSsr(getServerSideProps); + export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper, + getData, }); diff --git a/apps/web/app/future/settings/organizations/[id]/add-teams/page.tsx b/apps/web/app/future/settings/organizations/[id]/add-teams/page.tsx index 0fd0d5d11b8809..86c8f3296295a8 100644 --- a/apps/web/app/future/settings/organizations/[id]/add-teams/page.tsx +++ b/apps/web/app/future/settings/organizations/[id]/add-teams/page.tsx @@ -1,6 +1,9 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization"; + import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/add-teams-view"; export const generateMetadata = async () => @@ -9,7 +12,10 @@ export const generateMetadata = async () => (t) => t("create_your_teams_description") ); +const getData = withAppDirSsr(getServerSideProps); + export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper, + getData, }); diff --git a/apps/web/app/future/settings/organizations/[id]/onboard-members/page.tsx b/apps/web/app/future/settings/organizations/[id]/onboard-members/page.tsx index 113cd399bec7f2..56be6482e10470 100644 --- a/apps/web/app/future/settings/organizations/[id]/onboard-members/page.tsx +++ b/apps/web/app/future/settings/organizations/[id]/onboard-members/page.tsx @@ -1,6 +1,9 @@ +import { withAppDirSsr } from "app/WithAppDirSsr"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization"; + import LegacyPage, { LayoutWrapper } from "~/settings/organizations/[id]/onboard-members-view"; export const generateMetadata = async () => @@ -9,7 +12,10 @@ export const generateMetadata = async () => (t) => t("invite_organization_admins_description") ); +const getData = withAppDirSsr(getServerSideProps); + export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper, + getData, }); diff --git a/apps/web/app/future/settings/(settings)/organizations/appearance/page.tsx b/apps/web/app/future/settings/organizations/appearance/page.tsx similarity index 77% rename from apps/web/app/future/settings/(settings)/organizations/appearance/page.tsx rename to apps/web/app/future/settings/organizations/appearance/page.tsx index 5f8b2547f0a43e..84acb6502b503c 100644 --- a/apps/web/app/future/settings/(settings)/organizations/appearance/page.tsx +++ b/apps/web/app/future/settings/organizations/appearance/page.tsx @@ -1,4 +1,5 @@ import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; import Page from "@calcom/features/ee/organizations/pages/settings/appearance"; @@ -8,4 +9,4 @@ export const generateMetadata = async () => (t) => t("appearance_org_description") ); -export default Page; +export default WithLayout({ Page }); diff --git a/apps/web/app/future/settings/(settings)/teams/[id]/event-type/page.tsx b/apps/web/app/future/settings/teams/[id]/event-type/page.tsx similarity index 59% rename from apps/web/app/future/settings/(settings)/teams/[id]/event-type/page.tsx rename to apps/web/app/future/settings/teams/[id]/event-type/page.tsx index c741324ceb8596..47e5b8b8c12a6a 100644 --- a/apps/web/app/future/settings/(settings)/teams/[id]/event-type/page.tsx +++ b/apps/web/app/future/settings/teams/[id]/event-type/page.tsx @@ -1,11 +1,12 @@ -import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/event-type"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import CreateTeamEventType, { GetLayout } from "~/settings/teams/[id]/event-types-view"; + export const generateMetadata = async () => await _generateMetadata( (t) => t("add_new_team_event_type"), (t) => t("new_event_type_to_book_description") ); -export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">; +export default WithLayout({ Page: CreateTeamEventType, getLayout: GetLayout })<"P">; diff --git a/apps/web/app/future/settings/(settings)/teams/[id]/onboard-members/page.tsx b/apps/web/app/future/settings/teams/[id]/onboard-members/page.tsx similarity index 58% rename from apps/web/app/future/settings/(settings)/teams/[id]/onboard-members/page.tsx rename to apps/web/app/future/settings/teams/[id]/onboard-members/page.tsx index adb33f8b63788d..cf2117b4ad0ef7 100644 --- a/apps/web/app/future/settings/(settings)/teams/[id]/onboard-members/page.tsx +++ b/apps/web/app/future/settings/teams/[id]/onboard-members/page.tsx @@ -1,11 +1,12 @@ -import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/onboard-members"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import AddNewTeamMembers, { GetLayout } from "~/settings/teams/[id]/onboard-members-view"; + export const generateMetadata = async () => await _generateMetadata( (t) => t("add_team_members"), (t) => t("add_team_members_description") ); -export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">; +export default WithLayout({ Page: AddNewTeamMembers, getLayout: GetLayout })<"P">; diff --git a/apps/web/app/future/settings/(settings)/teams/new/page.tsx b/apps/web/app/future/settings/teams/new/page.tsx similarity index 57% rename from apps/web/app/future/settings/(settings)/teams/new/page.tsx rename to apps/web/app/future/settings/teams/new/page.tsx index 592517ab488227..160d6559d102c6 100644 --- a/apps/web/app/future/settings/(settings)/teams/new/page.tsx +++ b/apps/web/app/future/settings/teams/new/page.tsx @@ -1,11 +1,12 @@ -import LegacyPage, { LayoutWrapper } from "@pages/settings/teams/new/index"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; +import CreateNewTeamView, { LayoutWrapper } from "~/settings/teams/new/create-new-team-view"; + export const generateMetadata = async () => await _generateMetadata( (t) => t("create_new_team"), (t) => t("create_new_team_description") ); -export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper })<"P">; +export default WithLayout({ Page: CreateNewTeamView, getLayout: LayoutWrapper })<"P">; diff --git a/apps/web/app/future/signup/page.tsx b/apps/web/app/future/signup/page.tsx index d24988d1d855bf..ff1a9cd80a78c8 100644 --- a/apps/web/app/future/signup/page.tsx +++ b/apps/web/app/future/signup/page.tsx @@ -1,10 +1,12 @@ -import LegacyPage, { type SignupProps } from "@pages/signup"; import { withAppDirSsr } from "app/WithAppDirSsr"; import { _generateMetadata } from "app/_utils"; import { WithLayout } from "app/layoutHOC"; import { getServerSideProps } from "@lib/signup/getServerSideProps"; +import type { SignupProps } from "~/signup-view"; +import Signup from "~/signup-view"; + export const generateMetadata = async () => await _generateMetadata( (t) => t("sign_up"), @@ -14,7 +16,7 @@ export const generateMetadata = async () => const getData = withAppDirSsr(getServerSideProps); export default WithLayout({ - Page: LegacyPage, + Page: Signup, getLayout: null, getData, })<"P">; diff --git a/apps/web/app/layoutHOC.tsx b/apps/web/app/layoutHOC.tsx index cd7070b270880c..8f71154e316424 100644 --- a/apps/web/app/layoutHOC.tsx +++ b/apps/web/app/layoutHOC.tsx @@ -51,6 +51,7 @@ export function WithLayout>({ requiresLicense={requiresLicense || !!(Page && "requiresLicense" in Page && Page.requiresLicense)} nonce={nonce} themeBasis={null} + isThemeSupported={!!(Page && "isThemeSupported" in Page && Page.isThemeSupported)} isBookingPage={isBookingPage || !!(Page && "isBookingPage" in Page && Page.isBookingPage)} {...props}> {pageWithServerLayout} diff --git a/apps/web/components/apps/AppPage.tsx b/apps/web/components/apps/AppPage.tsx index ac556f7045ca92..8294a0dff77118 100644 --- a/apps/web/components/apps/AppPage.tsx +++ b/apps/web/components/apps/AppPage.tsx @@ -177,7 +177,7 @@ export const AppPage = ({ typeof descriptionItem === "object" ? (
+ className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center overflow-hidden rounded-md last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full"> `, + }} + /> + + )} + + + ) : null} +
+
+ + {/* Left side */} +
+
+

+ {IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")} +

+ {IS_CALCOM ? ( +

{t("cal_signup_description")}

+ ) : ( +

+ {t("calcom_explained", { + appName: APP_NAME, + })} +

+ )} +
+ {/* Form Container */} +
+
{ + let updatedValues = values; + if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) { + updatedValues = { + ...values, + username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail), + }; + } + await signUp(updatedValues); + }}> + {/* Username */} + {!isOrgInviteByLink ? ( + setUsernameTaken(value)} + data-testid="signup-usernamefield" + setPremium={(value) => setPremiumUsername(value)} + addOnLeading={ + orgSlug + ? `${getOrgFullOrigin(orgSlug, { protocol: true }).replace(URL_PROTOCOL_REGEX, "")}/` + : `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/` + } + /> + ) : null} + {/* Email */} + + + {/* Password */} + + {/* Cloudflare Turnstile Captcha */} + {CLOUDFLARE_SITE_ID ? ( + { + formMethods.setValue("cfToken", token); + }} + /> + ) : null} + + handleConsentChange(COOKIE_CONSENT)} + description={t("cookie_consent_checkbox")} + /> + {errors.apiError && ( + + )} + + + {!isGoogleLoginEnabled && !isSAMLLoginEnabled ? null : ( +
+
+
+ + {t("or_continue_with")} + +
+
+
+ )} +
+ {isGoogleLoginEnabled ? ( + + ) : null} + {isSAMLLoginEnabled ? ( + + ) : null} +
+
+ {/* Already have an account & T&C */} +
+
+
+

{t("already_have_account")}

+ + {t("sign_in")} + +
+
+ + Terms + , + + Privacy Policy. + , + ]} + /> +
+
+
+
+
+ {IS_CALCOM && ( + <> +
+
+ Cal.com was Product of the Day at ProductHunt +
+
+ Cal.com was Product of the Week at ProductHunt +
+
+ Cal.com was Product of the Month at ProductHunt +
+
+
+
+ ProductHunt Rating of 5 Stars +
+
+ Google Reviews Rating of 4.7 Stars +
+
+ G2 Rating of 4.7 Stars +
+
+ + )} +
+ Cal.com Booking Page + Cal.com Booking Page +
+
+ {FEATURES.map((feature) => ( + <> +
+
+ + {t(feature.title)} +
+
+

+ {t( + feature.description, + feature.i18nOptions && { + ...feature.i18nOptions, + } + )} +

+
+
+ + ))} +
+
+
+ +
+ + ); +} diff --git a/apps/web/package.json b/apps/web/package.json index 2ea2345dd84a51..ea4583360ccbca 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "4.5.0", + "version": "4.5.3", "private": true, "scripts": { "analyze": "ANALYZE=true next build", diff --git a/apps/web/pages/api/verify-booking-token.ts b/apps/web/pages/api/verify-booking-token.ts new file mode 100644 index 00000000000000..c220823235867f --- /dev/null +++ b/apps/web/pages/api/verify-booking-token.ts @@ -0,0 +1,97 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; +import { UserPermissionRole } from "@calcom/prisma/enums"; +import { TRPCError } from "@calcom/trpc/server"; +import { createContext } from "@calcom/trpc/server/createContext"; +import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router"; +import { createCallerFactory } from "@calcom/trpc/server/trpc"; +import type { UserProfile } from "@calcom/types/UserProfile"; + +enum DirectAction { + ACCEPT = "accept", + REJECT = "reject", +} + +const querySchema = z.object({ + action: z.nativeEnum(DirectAction), + token: z.string(), + bookingUid: z.string(), + userId: z.string(), +}); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { action, token, bookingUid, userId } = querySchema.parse(req.query); + // Rejections runs on a POST request, confirming on a GET request. + const { reason } = z.object({ reason: z.string().optional() }).parse(req.body || {}); + + const booking = await prisma.booking.findUnique({ + where: { oneTimePassword: token }, + }); + + if (!booking) { + res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent("Error confirming booking")}`); + return; + } + + const user = await prisma.user.findUniqueOrThrow({ + where: { id: Number(userId) }, + }); + + /** We shape the session as required by tRPC router */ + async function sessionGetter() { + return { + user: { + id: Number(userId), + username: "" /* Not used in this context */, + role: UserPermissionRole.USER, + /* Not used in this context */ + profile: { + id: null, + organizationId: null, + organization: null, + username: "", + upId: "", + } satisfies UserProfile, + }, + upId: "", + hasValidLicense: true, + expires: "" /* Not used in this context */, + }; + } + + try { + /** @see https://trpc.io/docs/server-side-calls */ + const createCaller = createCallerFactory(bookingsRouter); + const ctx = await createContext({ req, res }, sessionGetter); + const caller = createCaller({ + ...ctx, + req, + res, + user: { ...user, locale: user?.locale ?? "en" }, + }); + await caller.confirm({ + bookingId: booking.id, + recurringEventId: booking.recurringEventId || undefined, + confirmed: action === DirectAction.ACCEPT, + /** Ignored reason input unless we're rejecting */ + reason: action === DirectAction.REJECT ? reason : undefined, + }); + } catch (e) { + let message = "Error confirming booking"; + if (e instanceof TRPCError) message = (e as TRPCError).message; + res.redirect(`/booking/${booking.uid}?error=${encodeURIComponent(message)}`); + return; + } + + await prisma.booking.update({ + where: { id: booking.id }, + data: { oneTimePassword: null }, + }); + + res.redirect(`/booking/${booking.uid}`); +} + +export default defaultResponder(handler); diff --git a/apps/web/pages/auth/setup/index.tsx b/apps/web/pages/auth/setup/index.tsx index 1b7b3826b93297..a611c1bd375861 100644 --- a/apps/web/pages/auth/setup/index.tsx +++ b/apps/web/pages/auth/setup/index.tsx @@ -1,152 +1,22 @@ "use client"; -import { usePathname, useRouter } from "next/navigation"; -import { useState } from "react"; - -import AdminAppsList from "@calcom/features/apps/AdminAppsList"; -import { APP_NAME } from "@calcom/lib/constants"; -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import { Meta, WizardForm } from "@calcom/ui"; - -import PageWrapper from "@components/PageWrapper"; -import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser"; -import ChooseLicense from "@components/setup/ChooseLicense"; -import EnterpriseLicense from "@components/setup/EnterpriseLicense"; +import { Meta } from "@calcom/ui"; import { getServerSideProps } from "@server/lib/setup/getServerSideProps"; -function useSetStep() { - const router = useRouter(); - const searchParams = useCompatSearchParams(); - const pathname = usePathname(); - const setStep = (newStep = 1) => { - const _searchParams = new URLSearchParams(searchParams ?? undefined); - _searchParams.set("step", newStep.toString()); - router.replace(`${pathname}?${_searchParams.toString()}`); - }; - return setStep; -} +import type { PageProps } from "~/auth/setup-view"; +import Setup from "~/auth/setup-view"; -export function Setup(props: inferSSRProps) { +const Page = (props: PageProps) => { const { t } = useLocale(); - const router = useRouter(); - const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE"); - const isFreeLicense = value === "FREE"; - const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense); - const setStep = useSetStep(); - - const steps: React.ComponentProps["steps"] = [ - { - title: t("administrator_user"), - description: t("lets_create_first_administrator_user"), - content: (setIsPending) => ( - { - setIsPending(true); - }} - onSuccess={() => { - setStep(2); - }} - onError={() => { - setIsPending(false); - }} - userCount={props.userCount} - /> - ), - }, - { - title: t("choose_a_license"), - description: t("choose_license_description"), - content: (setIsPending) => { - return ( - { - setIsPending(true); - setStep(3); - }} - /> - ); - }, - }, - ]; - - if (!isFreeLicense) { - steps.push({ - title: t("step_enterprise_license"), - description: t("step_enterprise_license_description"), - content: (setIsPending) => { - const currentStep = 3; - return ( - { - setIsPending(true); - }} - onSuccess={() => { - setStep(currentStep + 1); - }} - onSuccessValidate={() => { - setIsEnabledEE(true); - }} - /> - ); - }, - isEnabled: isEnabledEE, - }); - } - - steps.push({ - title: t("enable_apps"), - description: t("enable_apps_description", { appName: APP_NAME }), - contentClassname: "!pb-0 mb-[-1px]", - content: (setIsPending) => { - const currentStep = isFreeLicense ? 3 : 4; - return ( - { - setIsPending(true); - router.replace("/"); - }} - /> - ); - }, - }); - return ( <> -
- t("current_step_of_total", { currentStep, maxSteps })} - /> -
+ ); -} - -Setup.isThemeSupported = false; -Setup.PageWrapper = PageWrapper; -export default Setup; +}; +export default Page; export { getServerSideProps }; diff --git a/apps/web/pages/auth/signin.tsx b/apps/web/pages/auth/signin.tsx index 86a26a60cd34ab..7ce8e789e85cda 100644 --- a/apps/web/pages/auth/signin.tsx +++ b/apps/web/pages/auth/signin.tsx @@ -1,34 +1,14 @@ -"use client"; - -import type { getProviders } from "next-auth/react"; -import { signIn } from "next-auth/react"; - -import { Button } from "@calcom/ui"; - import PageWrapper from "@components/PageWrapper"; import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps"; -function signin({ providers }: { providers: Awaited> }) { - if (!providers) { - return null; - } +import type { PageProps } from "~/auth/signin-view"; +import SignIn from "~/auth/signin-view"; - return ( -
- {Object.values(providers).map((provider) => { - return ( -
- -
- ); - })} -
- ); -} +const Page = (props: PageProps) => ; -signin.PageWrapper = PageWrapper; +Page.PageWrapper = PageWrapper; -export default signin; +export default Page; export { getServerSideProps }; diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx index 5ea01f8dc9b46f..8671d02292fde4 100644 --- a/apps/web/pages/auth/sso/[provider].tsx +++ b/apps/web/pages/auth/sso/[provider].tsx @@ -1,47 +1,12 @@ -"use client"; - -import { signIn } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; - -import type { inferSSRProps } from "@lib/types/inferSSRProps"; - import PageWrapper from "@components/PageWrapper"; import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps"; -export type SSOProviderPageProps = inferSSRProps; - -export default function Provider(props: SSOProviderPageProps) { - const searchParams = useCompatSearchParams(); - const router = useRouter(); - - useEffect(() => { - const email = searchParams?.get("email"); - if (props.provider === "saml") { - if (!email) { - router.push(`/auth/error?error=Email not provided`); - return; - } - - if (!props.isSAMLLoginEnabled) { - router.push(`/auth/error?error=SAML login not enabled`); - return; - } - - signIn("saml", {}, { tenant: props.tenant, product: props.product }); - } else if (props.provider === "google" && email) { - signIn("google", {}, { login_hint: email }); - } else { - signIn(props.provider); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return null; -} +import type { SSOProviderPageProps } from "~/auth/sso/provider-view"; +import SSOProviderView from "~/auth/sso/provider-view"; -Provider.PageWrapper = PageWrapper; +const Page = (props: SSOProviderPageProps) => ; +Page.PageWrapper = PageWrapper; +export default Page; export { getServerSideProps }; diff --git a/apps/web/pages/auth/sso/direct.tsx b/apps/web/pages/auth/sso/direct.tsx index b616c592503c5b..c5b0f1aeecea77 100644 --- a/apps/web/pages/auth/sso/direct.tsx +++ b/apps/web/pages/auth/sso/direct.tsx @@ -1,44 +1,12 @@ -"use client"; - -import { signIn } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; - -import type { inferSSRProps } from "@lib/types/inferSSRProps"; - import PageWrapper from "@components/PageWrapper"; -import type { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps"; - -// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider. -// Accessible only on self-hosted Cal.com instances. -export default function Page({ samlTenantID, samlProductID }: inferSSRProps) { - const router = useRouter(); +import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps"; - useEffect(() => { - if (HOSTED_CAL_FEATURES) { - router.push("/auth/login"); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +import type { SSODirectPageProps } from "~/auth/sso/direct-view"; +import SSODirectView from "~/auth/sso/direct-view"; - useEffect(() => { - // Initiate SAML authentication flow - signIn( - "saml", - { - callbackUrl: "/", - }, - { tenant: samlTenantID, product: samlProductID } - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return null; -} - -export { getServerSideProps }; +const Page = (props: SSODirectPageProps) => ; Page.PageWrapper = PageWrapper; +export default Page; +export { getServerSideProps }; diff --git a/apps/web/pages/d/[link]/[slug].tsx b/apps/web/pages/d/[link]/[slug].tsx index 20233baca17d87..1a9e3cf1369185 100644 --- a/apps/web/pages/d/[link]/[slug].tsx +++ b/apps/web/pages/d/[link]/[slug].tsx @@ -1,48 +1,11 @@ -"use client"; - -import { Booker } from "@calcom/atoms/monorepo"; -import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses"; -import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo"; - import { getServerSideProps, type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps"; import PageWrapper from "@components/PageWrapper"; -export default function Type({ - slug, - isEmbed, - user, - booking, - isBrandingHidden, - isTeamEvent, - entity, - duration, - hashedLink, -}: PageProps) { - return ( -
- - -
- ); -} +import Type from "~/d/[link]/d-type-view"; +const Page = (props: PageProps) => ; export { getServerSideProps }; -Type.PageWrapper = PageWrapper; -Type.isBookingPage = true; +Page.PageWrapper = PageWrapper; +export default Page; diff --git a/apps/web/pages/insights/index.tsx b/apps/web/pages/insights/index.tsx index c98b0411e64c68..fa860836ac57b5 100644 --- a/apps/web/pages/insights/index.tsx +++ b/apps/web/pages/insights/index.tsx @@ -1,123 +1,13 @@ "use client"; -import { - AverageEventDurationChart, - BookingKPICards, - BookingStatusLineChart, - LeastBookedTeamMembersTable, - MostBookedTeamMembersTable, - PopularEventsTable, - HighestNoShowHostTable, - RecentFeedbackTable, - HighestRatedMembersTable, - LowestRatedMembersTable, -} from "@calcom/features/insights/components"; -import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider"; -import { Filters } from "@calcom/features/insights/filters"; -import Shell from "@calcom/features/shell/Shell"; -import { UpgradeTip } from "@calcom/features/tips"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc"; -import { Button, ButtonGroup } from "@calcom/ui"; -import { Icon } from "@calcom/ui"; - import { getServerSideProps } from "@lib/insights/getServerSideProps"; import PageWrapper from "@components/PageWrapper"; -export default function InsightsPage() { - const { t } = useLocale(); - const { data: user } = trpc.viewer.me.useQuery(); - - const features = [ - { - icon: , - title: t("view_bookings_across"), - description: t("view_bookings_across_description"), - }, - { - icon: , - title: t("identify_booking_trends"), - description: t("identify_booking_trends_description"), - }, - { - icon: , - title: t("spot_popular_event_types"), - description: t("spot_popular_event_types_description"), - }, - ]; - - return ( -
- - - - - - -
- }> - {!user ? ( - <> - ) : ( - - - -
- - - - -
- - - -
-
- - -
- -
- - - -
- - {t("looking_for_more_insights")}{" "} - - {" "} - {t("contact_support")} - - -
-
- )} - - -
- ); -} - -InsightsPage.PageWrapper = PageWrapper; +import type { PageProps } from "~/insights/insights-view"; +import InsightsPage from "~/insights/insights-view"; +const Page = (props: PageProps) => ; +Page.PageWrapper = PageWrapper; +export default Page; export { getServerSideProps }; diff --git a/apps/web/pages/reschedule/[uid].tsx b/apps/web/pages/reschedule/[uid].tsx index 371afa924d3d36..1105adaa4cab41 100644 --- a/apps/web/pages/reschedule/[uid].tsx +++ b/apps/web/pages/reschedule/[uid].tsx @@ -1,185 +1,6 @@ -// page can be a server component -import type { GetServerSidePropsContext } from "next"; -import { URLSearchParams } from "url"; -import { z } from "zod"; - -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking"; -import { getDefaultEvent } from "@calcom/lib/defaultEvents"; -import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat"; -import { UserRepository } from "@calcom/lib/server/repository/user"; -import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import { BookingStatus } from "@calcom/prisma/client"; - export default function Type() { // Just redirect to the schedule page to reschedule it. return null; } -const querySchema = z.object({ - uid: z.string(), - seatReferenceUid: z.string().optional(), - rescheduledBy: z.string().optional(), - allowRescheduleForCancelledBooking: z - .string() - .transform((value) => value === "true") - .optional(), -}); - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await getServerSession(context); - - const { - uid: bookingUid, - seatReferenceUid, - rescheduledBy, - /** - * This is for the case of request-reschedule where the booking is cancelled - */ - allowRescheduleForCancelledBooking, - } = querySchema.parse(context.query); - - const coepFlag = context.query["flag.coep"]; - const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat( - prisma, - bookingUid - ); - - const booking = await prisma.booking.findUnique({ - where: { - uid, - }, - select: { - ...bookingMinimalSelect, - eventType: { - select: { - users: { - select: { - username: true, - }, - }, - slug: true, - team: { - select: { - parentId: true, - slug: true, - }, - }, - seatsPerTimeSlot: true, - userId: true, - owner: { - select: { - id: true, - }, - }, - hosts: { - select: { - user: { - select: { - id: true, - }, - }, - }, - }, - }, - }, - dynamicEventSlugRef: true, - dynamicGroupSlugRef: true, - user: true, - status: true, - }, - }); - const dynamicEventSlugRef = booking?.dynamicEventSlugRef || ""; - - if (!booking) { - return { - notFound: true, - } as const; - } - - // If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details. - // A booking that has been rescheduled to a new booking will also have a status of CANCELLED - if ( - !allowRescheduleForCancelledBooking && - (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) - ) { - return { - redirect: { - destination: `/booking/${uid}`, - permanent: false, - }, - }; - } - - if (!booking?.eventType && !booking?.dynamicEventSlugRef) { - // TODO: Show something in UI to let user know that this booking is not rescheduleable - return { - notFound: true, - } as { - notFound: true; - }; - } - - // if booking event type is for a seated event and no seat reference uid is provided, throw not found - if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) { - const userId = session?.user?.id; - - if (!userId && !seatReferenceUid) { - return { - redirect: { - destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`, - permanent: false, - }, - }; - } - const userIsHost = booking?.eventType.hosts.find((host) => { - if (host.user.id === userId) return true; - }); - - const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId; - - if (!userIsHost && !userIsOwnerOfEventType) { - return { - notFound: true, - } as { - notFound: true; - }; - } - } - - const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef); - - const enrichedBookingUser = booking.user - ? await UserRepository.enrichUserWithItsProfile({ user: booking.user }) - : null; - - const eventUrl = await buildEventUrlFromBooking({ - eventType, - dynamicGroupSlugRef: booking.dynamicGroupSlugRef ?? null, - profileEnrichedBookingUser: enrichedBookingUser, - }); - - const destinationUrlSearchParams = new URLSearchParams(); - - destinationUrlSearchParams.set("rescheduleUid", seatReferenceUid || bookingUid); - - // TODO: I think we should just forward all the query params here including coep flag - if (coepFlag) { - destinationUrlSearchParams.set("flag.coep", coepFlag as string); - } - - const currentUserEmail = rescheduledBy ?? session?.user?.email; - - if (currentUserEmail) { - destinationUrlSearchParams.set("rescheduledBy", currentUserEmail); - } - - return { - redirect: { - destination: `${eventUrl}?${destinationUrlSearchParams.toString()}${ - eventType.seatsPerTimeSlot ? "&bookingUid=null" : "" - }`, - permanent: false, - }, - }; -} +export { getServerSideProps } from "@lib/reschedule/[uid]/getServerSideProps"; diff --git a/apps/web/pages/reschedule/[uid]/embed.tsx b/apps/web/pages/reschedule/[uid]/embed.tsx index 034b8ee719dc0d..5d6b405e57085f 100644 --- a/apps/web/pages/reschedule/[uid]/embed.tsx +++ b/apps/web/pages/reschedule/[uid]/embed.tsx @@ -1,5 +1,3 @@ -"use client"; - import withEmbedSsr from "@lib/withEmbedSsr"; import { getServerSideProps as _getServerSideProps } from "../[uid]"; diff --git a/apps/web/pages/settings/organizations/billing.tsx b/apps/web/pages/settings/organizations/billing.tsx index d830b3f3ac597b..d44849eea252a1 100644 --- a/apps/web/pages/settings/organizations/billing.tsx +++ b/apps/web/pages/settings/organizations/billing.tsx @@ -1,9 +1,3 @@ -import type { CalPageWrapper } from "@components/PageWrapper"; -import PageWrapper from "@components/PageWrapper"; - import BillingPage from "../../settings/billing/index"; -const Page = BillingPage as CalPageWrapper; -Page.PageWrapper = PageWrapper; - -export default Page; +export default BillingPage; diff --git a/apps/web/pages/settings/organizations/general.tsx b/apps/web/pages/settings/organizations/general.tsx index 308eecf72ff6a1..e5d5cefb108785 100644 --- a/apps/web/pages/settings/organizations/general.tsx +++ b/apps/web/pages/settings/organizations/general.tsx @@ -1,9 +1,11 @@ import OrgGeneralView from "@calcom/features/ee/organizations/pages/settings/general"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = OrgGeneralView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/members.tsx b/apps/web/pages/settings/organizations/members.tsx index a175173e5d1a85..34061d4a8c4331 100644 --- a/apps/web/pages/settings/organizations/members.tsx +++ b/apps/web/pages/settings/organizations/members.tsx @@ -1,6 +1,6 @@ import MembersView from "@calcom/features/ee/organizations/pages/settings/members"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; export { @@ -8,7 +8,9 @@ export { type PageProps, } from "@calcom/features/ee/organizations/pages/settings/getServerSidePropsMembers"; -const Page = MembersView as unknown as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/profile.tsx b/apps/web/pages/settings/organizations/profile.tsx index ef14c3b95711a6..e6bc8e8c1a0a95 100644 --- a/apps/web/pages/settings/organizations/profile.tsx +++ b/apps/web/pages/settings/organizations/profile.tsx @@ -1,9 +1,11 @@ import OrgProfileView from "@calcom/features/ee/organizations/pages/settings/profile"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = OrgProfileView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/sso.tsx b/apps/web/pages/settings/organizations/sso.tsx index 5b3b506c42e0b2..96f87e2077f5e9 100644 --- a/apps/web/pages/settings/organizations/sso.tsx +++ b/apps/web/pages/settings/organizations/sso.tsx @@ -1,9 +1,11 @@ import OrgSSOView from "@calcom/features/ee/sso/page/orgs-sso-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = OrgSSOView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx b/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx index 6c6343e79198ad..ea0d5d2cc0fd83 100644 --- a/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx +++ b/apps/web/pages/settings/organizations/teams/other/[id]/appearance.tsx @@ -1,9 +1,11 @@ import TeamAppearenceView from "@calcom/features/ee/teams/pages/team-appearance-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = TeamAppearenceView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx b/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx index 14c0026a467879..fb4357175d1440 100644 --- a/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx +++ b/apps/web/pages/settings/organizations/teams/other/[id]/members.tsx @@ -1,9 +1,11 @@ import TeamMembersView from "@calcom/features/ee/organizations/pages/settings/other-team-members-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = TeamMembersView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx b/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx index ad4cba98e58a5a..c24a0104b0f9f6 100644 --- a/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx +++ b/apps/web/pages/settings/organizations/teams/other/[id]/profile.tsx @@ -1,9 +1,11 @@ import OtherTeamProfileView from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = OtherTeamProfileView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/organizations/teams/other/index.ts b/apps/web/pages/settings/organizations/teams/other/index.tsx similarity index 58% rename from apps/web/pages/settings/organizations/teams/other/index.ts rename to apps/web/pages/settings/organizations/teams/other/index.tsx index 06052c1f3e1fbc..84b951e9e86685 100644 --- a/apps/web/pages/settings/organizations/teams/other/index.ts +++ b/apps/web/pages/settings/organizations/teams/other/index.tsx @@ -1,9 +1,11 @@ import OtherTeamListView from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import PageWrapper from "@components/PageWrapper"; -import type { CalPageWrapper } from "@components/PageWrapper"; -const Page = OtherTeamListView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/platform/billing/index.tsx b/apps/web/pages/settings/platform/billing/index.tsx new file mode 100644 index 00000000000000..4bdf02297b9bcf --- /dev/null +++ b/apps/web/pages/settings/platform/billing/index.tsx @@ -0,0 +1,12 @@ +import PageWrapper from "@components/PageWrapper"; + +import PlatformBillingUpgrade from "~/settings/platform/billing/billing-view"; + +const Page = new Proxy<{ + (): JSX.Element; + PageWrapper?: typeof PageWrapper; +}>(PlatformBillingUpgrade, {}); + +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/settings/platform/plans/index.tsx b/apps/web/pages/settings/platform/plans/index.tsx new file mode 100644 index 00000000000000..8aa52be3e527ef --- /dev/null +++ b/apps/web/pages/settings/platform/plans/index.tsx @@ -0,0 +1,12 @@ +import PageWrapper from "@components/PageWrapper"; + +import PlatformPlansView from "~/settings/platform/plans/platform-plans-view"; + +const Page = new Proxy<{ + (): JSX.Element; + PageWrapper?: typeof PageWrapper; +}>(PlatformPlansView, {}); + +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/settings/teams/[id]/appearance.tsx b/apps/web/pages/settings/teams/[id]/appearance.tsx index 6c6343e79198ad..ea0d5d2cc0fd83 100644 --- a/apps/web/pages/settings/teams/[id]/appearance.tsx +++ b/apps/web/pages/settings/teams/[id]/appearance.tsx @@ -1,9 +1,11 @@ import TeamAppearenceView from "@calcom/features/ee/teams/pages/team-appearance-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = TeamAppearenceView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/teams/[id]/billing.tsx b/apps/web/pages/settings/teams/[id]/billing.tsx index baa9b4a4275683..ccd9d3c3693f48 100644 --- a/apps/web/pages/settings/teams/[id]/billing.tsx +++ b/apps/web/pages/settings/teams/[id]/billing.tsx @@ -1,9 +1,3 @@ -import type { CalPageWrapper } from "@components/PageWrapper"; -import PageWrapper from "@components/PageWrapper"; - import BillingPage from "../../billing"; -const Page = BillingPage as CalPageWrapper; -Page.PageWrapper = PageWrapper; - -export default Page; +export default BillingPage; diff --git a/apps/web/pages/settings/teams/[id]/event-type.tsx b/apps/web/pages/settings/teams/[id]/event-type.tsx index 101fa610423ddc..ec0a2b1c3bbc34 100644 --- a/apps/web/pages/settings/teams/[id]/event-type.tsx +++ b/apps/web/pages/settings/teams/[id]/event-type.tsx @@ -1,54 +1,12 @@ "use client"; import Head from "next/head"; -import { useRouter } from "next/navigation"; -import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm"; -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { WizardLayout } from "@calcom/ui"; -import { Button, showToast } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; -export const CreateTeamEventType = () => { - const searchParams = useCompatSearchParams(); - const { t } = useLocale(); - const router = useRouter(); - const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1; - - const onSuccessMutation = () => { - router.push(`/settings/teams/${teamId}/profile`); - }; - - const onErrorMutation = (err: string) => { - showToast(err, "error"); - }; - - const SubmitButton = (isPending: boolean) => { - return ( - - ); - }; - - return ( - - ); -}; +import CreateTeamEventType, { GetLayout } from "~/settings/teams/[id]/event-types-view"; const TeamEventTypePage = () => { const { t } = useLocale(); @@ -63,23 +21,6 @@ const TeamEventTypePage = () => { ); }; -export const GetLayout = (page: React.ReactElement) => { - const router = useRouter(); - const searchParams = useCompatSearchParams(); - const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1; - - return ( - { - router.push(`/settings/teams/${teamId}/profile`); - }}> - {page} - - ); -}; - TeamEventTypePage.getLayout = GetLayout; TeamEventTypePage.PageWrapper = PageWrapper; diff --git a/apps/web/pages/settings/teams/[id]/members.tsx b/apps/web/pages/settings/teams/[id]/members.tsx index 5381265926a595..3a56a6025c8991 100644 --- a/apps/web/pages/settings/teams/[id]/members.tsx +++ b/apps/web/pages/settings/teams/[id]/members.tsx @@ -1,9 +1,11 @@ import TeamMembersView from "@calcom/features/ee/teams/pages/team-members-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = TeamMembersView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/teams/[id]/onboard-members.tsx b/apps/web/pages/settings/teams/[id]/onboard-members.tsx index aaf3dd9bfaf359..ca352381b85d9f 100644 --- a/apps/web/pages/settings/teams/[id]/onboard-members.tsx +++ b/apps/web/pages/settings/teams/[id]/onboard-members.tsx @@ -2,12 +2,12 @@ import Head from "next/head"; -import AddNewTeamMembers from "@calcom/features/ee/teams/components/AddNewTeamMembers"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { WizardLayout } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; +import AddNewTeamMembers, { GetLayout } from "~/settings/teams/[id]/onboard-members-view"; + const OnboardTeamMembersPage = () => { const { t } = useLocale(); return ( @@ -21,12 +21,6 @@ const OnboardTeamMembersPage = () => { ); }; -export const GetLayout = (page: React.ReactElement) => ( - - {page} - -); - OnboardTeamMembersPage.getLayout = GetLayout; OnboardTeamMembersPage.PageWrapper = PageWrapper; diff --git a/apps/web/pages/settings/teams/[id]/profile.tsx b/apps/web/pages/settings/teams/[id]/profile.tsx index 9845bac378b0bc..d69987c83a1d52 100644 --- a/apps/web/pages/settings/teams/[id]/profile.tsx +++ b/apps/web/pages/settings/teams/[id]/profile.tsx @@ -1,9 +1,11 @@ import TeamProfileView from "@calcom/features/ee/teams/pages/team-profile-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = TeamProfileView as CalPageWrapper; +const Page = () => ; + +Page.getLayout = getLayout; Page.PageWrapper = PageWrapper; export default Page; diff --git a/apps/web/pages/settings/teams/index.ts b/apps/web/pages/settings/teams/index.ts deleted file mode 100644 index 2f00167179c5f7..00000000000000 --- a/apps/web/pages/settings/teams/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import TeamListView from "@calcom/features/ee/teams/pages/team-listing-view"; - -import PageWrapper from "@components/PageWrapper"; -import type { CalPageWrapper } from "@components/PageWrapper"; - -const Page = TeamListView as CalPageWrapper; -Page.PageWrapper = PageWrapper; - -export default Page; diff --git a/apps/web/pages/settings/teams/index.tsx b/apps/web/pages/settings/teams/index.tsx new file mode 100644 index 00000000000000..ba0d9b85e23d5a --- /dev/null +++ b/apps/web/pages/settings/teams/index.tsx @@ -0,0 +1,11 @@ +import TeamListingView from "@calcom/features/ee/teams/pages/team-listing-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; + +import PageWrapper from "@components/PageWrapper"; + +const Page = () => ; + +Page.getLayout = getLayout; +Page.PageWrapper = PageWrapper; + +export default Page; diff --git a/apps/web/pages/settings/teams/new/index.tsx b/apps/web/pages/settings/teams/new/index.tsx index a019cdfbbe2b8c..07cacbfee557a9 100644 --- a/apps/web/pages/settings/teams/new/index.tsx +++ b/apps/web/pages/settings/teams/new/index.tsx @@ -1,50 +1,15 @@ "use client"; import Head from "next/head"; -import { useRouter } from "next/navigation"; -import { z } from "zod"; -import { CreateANewTeamForm } from "@calcom/features/ee/teams/components"; -import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; -import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; -import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; -import type { RouterOutputs } from "@calcom/trpc/react"; -import { WizardLayout } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; -const querySchema = z.object({ - returnTo: z.string().optional(), - slug: z.string().optional(), -}); +import CreateNewTeamView, { LayoutWrapper } from "~/settings/teams/new/create-new-team-view"; const CreateNewTeamPage = () => { const { t } = useLocale(); - const params = useParamsWithFallback(); - const parsedQuery = querySchema.safeParse(params); - const router = useRouter(); - const telemetry = useTelemetry(); - - const isTeamBillingEnabledClient = !!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY && HOSTED_CAL_FEATURES; - const flag = isTeamBillingEnabledClient - ? { - telemetryEvent: telemetryEventTypes.team_checkout_session_created, - submitLabel: "checkout", - } - : { - telemetryEvent: telemetryEventTypes.team_created, - submitLabel: "continue", - }; - - const returnToParam = - (parsedQuery.success ? getSafeRedirectUrl(parsedQuery.data.returnTo) : "/teams") || "/teams"; - - const onSuccess = (data: RouterOutputs["viewer"]["teams"]["create"]) => { - telemetry.event(flag.telemetryEvent); - router.push(data.url); - }; return ( <> @@ -52,22 +17,10 @@ const CreateNewTeamPage = () => { {t("create_new_team")} - router.push(returnToParam)} - submitLabel={flag.submitLabel} - onSuccess={onSuccess} - /> + ); }; -export const LayoutWrapper = (page: React.ReactElement) => { - return ( - - {page} - - ); -}; CreateNewTeamPage.getLayout = LayoutWrapper; CreateNewTeamPage.PageWrapper = PageWrapper; diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index 60c6d8eb02f75f..ecf0419ac38806 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -1,667 +1,12 @@ -"use client"; - -import { Analytics as DubAnalytics } from "@dub/analytics/react"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { signIn } from "next-auth/react"; -import { Trans } from "next-i18next"; -import dynamic from "next/dynamic"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import Script from "next/script"; -import { useState, useEffect } from "react"; -import type { SubmitHandler } from "react-hook-form"; -import { useForm, useFormContext } from "react-hook-form"; -import { Toaster } from "react-hot-toast"; -import { z } from "zod"; - -import getStripe from "@calcom/app-store/stripepayment/lib/client"; -import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; -import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; -import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { classNames } from "@calcom/lib"; -import { - APP_NAME, - URL_PROTOCOL_REGEX, - IS_CALCOM, - IS_EUROPE, - WEBAPP_URL, - CLOUDFLARE_SITE_ID, - WEBSITE_PRIVACY_POLICY_URL, - WEBSITE_TERMS_URL, - WEBSITE_URL, -} from "@calcom/lib/constants"; -import { isENVDev } from "@calcom/lib/env"; -import { fetchUsername } from "@calcom/lib/fetchUsername"; -import { pushGTMEvent } from "@calcom/lib/gtm"; -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; -import { useDebounce } from "@calcom/lib/hooks/useDebounce"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; -import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils"; -import type { inferSSRProps } from "@calcom/types/inferSSRProps"; -import { - Button, - HeadSeo, - PasswordField, - TextField, - Form, - Alert, - showToast, - CheckboxField, - Icon, -} from "@calcom/ui"; - import { getServerSideProps } from "@lib/signup/getServerSideProps"; import PageWrapper from "@components/PageWrapper"; -const signupSchema = apiSignupSchema.extend({ - apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API - cfToken: z.string().optional(), -}); - -const TurnstileCaptcha = dynamic(() => import("@components/auth/Turnstile"), { ssr: false }); - -type FormValues = z.infer; - -export type SignupProps = inferSSRProps; - -const FEATURES = [ - { - title: "connect_all_calendars", - description: "connect_all_calendars_description", - i18nOptions: { - appName: APP_NAME, - }, - icon: "calendar-heart" as const, - }, - { - title: "set_availability", - description: "set_availbility_description", - icon: "users" as const, - }, - { - title: "share_a_link_or_embed", - description: "share_a_link_or_embed_description", - icon: "link-2" as const, - i18nOptions: { - appName: APP_NAME, - }, - }, -]; - -function UsernameField({ - username, - setPremium, - premium, - setUsernameTaken, - orgSlug, - usernameTaken, - disabled, - ...props -}: React.ComponentProps & { - username: string; - setPremium: (value: boolean) => void; - premium: boolean; - usernameTaken: boolean; - orgSlug?: string; - setUsernameTaken: (value: boolean) => void; -}) { - const { t } = useLocale(); - const { register, formState } = useFormContext(); - const debouncedUsername = useDebounce(username, 600); - - useEffect(() => { - if (formState.isSubmitting || formState.isSubmitSuccessful) return; - - async function checkUsername() { - // If the username can't be changed, there is no point in doing the username availability check - if (disabled) return; - if (!debouncedUsername) { - setPremium(false); - setUsernameTaken(false); - return; - } - fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => { - setPremium(data.premium); - setUsernameTaken(!data.available); - }); - } - checkUsername(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]); - - return ( -
- - {(!formState.isSubmitting || !formState.isSubmitted) && ( -
-
- {usernameTaken ? ( -
- -

{t("already_in_use_error")}

-
- ) : premium ? ( -
- -

- {t("premium_username", { - price: getPremiumPlanPriceValue(), - })} -

-
- ) : null} -
-
- )} -
- ); -} - -function addOrUpdateQueryParam(url: string, key: string, value: string) { - const separator = url.includes("?") ? "&" : "?"; - const param = `${key}=${encodeURIComponent(value)}`; - return `${url}${separator}${param}`; -} - -export default function Signup({ - prepopulateFormValues, - token, - orgSlug, - isGoogleLoginEnabled, - isSAMLLoginEnabled, - orgAutoAcceptEmail, - redirectUrl, - emailVerificationEnabled, -}: SignupProps) { - const [premiumUsername, setPremiumUsername] = useState(false); - const [usernameTaken, setUsernameTaken] = useState(false); - const [isGoogleLoading, setIsGoogleLoading] = useState(false); - const searchParams = useCompatSearchParams(); - const telemetry = useTelemetry(); - const { t, i18n } = useLocale(); - const router = useRouter(); - const formMethods = useForm({ - resolver: zodResolver(signupSchema), - defaultValues: prepopulateFormValues satisfies FormValues, - mode: "onChange", - }); - const { - register, - watch, - formState: { isSubmitting, errors, isSubmitSuccessful }, - } = formMethods; - - useEffect(() => { - if (redirectUrl) { - localStorage.setItem("onBoardingRedirect", redirectUrl); - } - }, [redirectUrl]); - - const [COOKIE_CONSENT, setCOOKIE_CONSENT] = useState(false); - - function handleConsentChange(consent: boolean) { - setCOOKIE_CONSENT(!consent); - } - - const loadingSubmitState = isSubmitSuccessful || isSubmitting; - - const handleErrorsAndStripe = async (resp: Response) => { - if (!resp.ok) { - const err = await resp.json(); - if (err.checkoutSessionId) { - const stripe = await getStripe(); - if (stripe) { - console.log("Redirecting to stripe checkout"); - const { error } = await stripe.redirectToCheckout({ - sessionId: err.checkoutSessionId, - }); - console.warn(error.message); - } - } else { - throw new Error(err.message); - } - } - }; +import type { SignupProps } from "~/signup-view"; +import Signup from "~/signup-view"; - const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username; - const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new"); - - const signUp: SubmitHandler = async (_data) => { - const { cfToken, ...data } = _data; - await fetch("/api/auth/signup", { - body: JSON.stringify({ - ...data, - language: i18n.language, - token, - }), - headers: { - "Content-Type": "application/json", - "cf-access-token": cfToken ?? "invalid-token", - }, - method: "POST", - }) - .then(handleErrorsAndStripe) - .then(async () => { - if (process.env.NEXT_PUBLIC_GTM_ID) - pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language }); - - telemetry.event(telemetryEventTypes.signup, collectPageParameters()); - - const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : "getting-started"; - const gettingStartedWithPlatform = "settings/platform/new"; - - const constructCallBackIfUrlPresent = () => { - if (isOrgInviteByLink) { - return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`; - } - - return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup"); - }; - - const constructCallBackIfUrlNotPresent = () => { - if (!!isPlatformUser) { - return `${WEBAPP_URL}/${gettingStartedWithPlatform}?from=signup`; - } - - return `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`; - }; - - const constructCallBackUrl = () => { - const callbackUrlSearchParams = searchParams?.get("callbackUrl"); - - return !!callbackUrlSearchParams - ? constructCallBackIfUrlPresent() - : constructCallBackIfUrlNotPresent(); - }; - - const callBackUrl = constructCallBackUrl(); - - await signIn<"credentials">("credentials", { - ...data, - callbackUrl: callBackUrl, - }); - }) - .catch((err) => { - formMethods.setError("apiError", { message: err.message }); - }); - }; - - return ( - <> - {IS_CALCOM && (!IS_EUROPE || COOKIE_CONSENT) ? ( - <> - {process.env.NEXT_PUBLIC_GTM_ID && ( - <> -